mod support; use levcs_core::{Blob, ObjectId}; use levcs_protocol::oracle::{SnapshotLeaseOracle, SnapshotLeasePhase}; use levcs_protocol::v2::*; use levcs_protocol::CanonicalCodec; use support::*; #[test] fn v2_ingestion_codecs_signatures_and_stable_retry_digest_are_frozen() { let signed = signed_normal_push(); roundtrip(&signed); signed.verify().unwrap(); let mut retry_operation = signed.operation.clone(); retry_operation.issued_at_micros += 10; retry_operation.nonce = [0x99; 16]; let retry = SignedPushOperationV2::sign(retry_operation, &key(1)).unwrap(); assert_ne!( signed.encode_canonical().unwrap(), retry.encode_canonical().unwrap() ); assert_eq!( signed.stable_digest().unwrap(), retry.stable_digest().unwrap() ); assert_ne!( signed.stable_digest().unwrap(), signed.operation.stable_digest(key(2).public().0).unwrap(), "the stable digest binds the signer" ); let mut tampered = signed.clone(); tampered.operation.pack_len += 1; assert!(tampered.verify().is_err()); let init = signed_init(); roundtrip(&init); init.verify().unwrap(); let mut different_genesis = init.clone(); different_genesis.operation.genesis_hash = id(99); assert_ne!( init.stable_digest().unwrap(), different_genesis.stable_digest().unwrap() ); } #[test] fn typed_ref_and_push_kind_invariants_reject_ambiguous_mutations() { let mut normal = signed_normal_push().operation; normal.projection_stage = Some(ProjectionStageRefV1 { session_id: [1; 16], manifest_digest: id(1), object_count: 1, object_bytes: 1, }); assert!(normal.encode_canonical().is_err()); let mut fork = signed_fork_push().operation; fork.authority_update = Some(id(2)); assert!(fork.encode_canonical().is_err()); let mut multi_ref_fork = signed_fork_push().operation; multi_ref_fork.updates.push(TypedRefCas { target: RefTarget::Branch("other".into()), expected: None, mutation: RefMutation::Set(id(23)), force: false, }); assert!(multi_ref_fork.encode_canonical().is_err()); let mut duplicate = signed_normal_push().operation; duplicate.updates.push(duplicate.updates[0].clone()); assert!(duplicate.encode_canonical().is_err()); let mut invalid_delete = signed_normal_push().operation; invalid_delete.updates = vec![TypedRefCas { target: RefTarget::Branch("main".into()), expected: None, mutation: RefMutation::Delete, force: false, }]; assert!(invalid_delete.encode_canonical().is_err()); let mut unsafe_name = signed_normal_push().operation; unsafe_name.updates[0].target = RefTarget::Branch("../authority".into()); assert!(unsafe_name.encode_canonical().is_err()); } #[test] fn retry_replay_and_terminal_retention_arithmetic_is_checked() { validate_retry_window(1_000, 2_000, 1_000, 100, 1_000).unwrap(); assert!(validate_retry_window(1_000, 2_001, 1_000, 100, 1_000).is_err()); assert!(validate_retry_window(i64::MAX, i64::MAX, i64::MAX, 1, 1).is_err()); assert!(validate_retry_window(1_000, 999, 1_000, 100, 1_000).is_err()); assert!(validate_retry_window(1_000, 1_100, 1_101, 200, 1_000).is_err()); assert!(matches!( validate_retry_window(999, 1_000, 1_100, 50, 1_000), Err(V2ValidationError::RetryWindow( "issued_at outside clock skew" )) )); assert!(matches!( validate_retry_window(1_200, 2_000, 1_100, 50, 1_000), Err(V2ValidationError::RetryWindow( "issued_at outside clock skew" )) )); assert_eq!( minimum_replay_retention_micros(60_000_000, 1_000_000).unwrap(), 121_000_000 ); validate_replay_retention(121_000_000, 60_000_000, 1_000_000).unwrap(); assert!(validate_replay_retention(120_999_999, 60_000_000, 1_000_000).is_err()); assert!(minimum_replay_retention_micros(i64::MAX, 1).is_err()); assert_eq!(receipt_visible_until(5_000, 4_500, 2_000).unwrap(), 6_500); assert_eq!(status_tombstone_until(6_500, 500).unwrap(), 7_000); assert!(receipt_visible_until(0, i64::MAX, 1).is_err()); assert!(status_tombstone_until(i64::MAX, 1).is_err()); } #[test] fn snapshots_are_deterministic_while_tokens_and_request_pins_are_ephemeral() { let signed_generation = snapshot(); roundtrip(&signed_generation); signed_generation.verify().unwrap(); let generation_digest = signed_generation.snapshot.generation_digest().unwrap(); let reader_key_digest = reader_key_digest_or_zero(Some(key(4).public().0)); let mac_key = [0xa5; 32]; let claims = SnapshotLeaseClaimsV1 { version: SNAPSHOT_LEASE_TOKEN_VERSION, mac_key_epoch: 3, token_id: [1; 16], repo_id: signed_generation.snapshot.repo_id, projection: signed_generation.snapshot.projection, generation_digest, high_repo_sequence: signed_generation.snapshot.high_repo_sequence, high_event_digest: signed_generation.snapshot.high_event_digest, issued_at_micros: 9_000, expires_at_micros: 10_000, reader_key_digest, }; let token = SnapshotLeaseTokenV1::mint(claims, &mac_key).unwrap(); roundtrip(&token); let expectation = SnapshotLeaseExpectationV1 { repo_id: signed_generation.snapshot.repo_id, generation_digest, projection: signed_generation.snapshot.projection, reader_key_digest, mac_key_epoch: 3, now_micros: 9_999, }; token.verify(&mac_key, expectation).unwrap(); assert!(token .verify( &mac_key, SnapshotLeaseExpectationV1 { repo_id: id(99), ..expectation }, ) .is_err()); assert!(token .verify( &mac_key, SnapshotLeaseExpectationV1 { now_micros: 10_001, ..expectation }, ) .is_err()); assert!( token.verify(&[0x5b; 32], expectation,).is_err(), "a wrong MAC key must be rejected even when every claim matches" ); let mut tampered_claims = token.clone(); tampered_claims.claims.repo_id = id(55); assert!( tampered_claims.verify(&mac_key, expectation).is_err(), "tampering with claims without re-minting the MAC must fail verification" ); let mut wrong_version = token.clone(); wrong_version.claims.version = SNAPSHOT_LEASE_TOKEN_VERSION + 1; assert!(wrong_version.verify(&mac_key, expectation).is_err()); let mut second_token = token.clone(); second_token.claims.token_id = [2; 16]; second_token = SnapshotLeaseTokenV1::mint(second_token.claims, &mac_key).unwrap(); let first = SnapshotResponseV1 { signed_generation: signed_generation.clone(), lease_token: token.encode_canonical().unwrap(), }; let second = SnapshotResponseV1 { signed_generation, lease_token: second_token.encode_canonical().unwrap(), }; assert_ne!( first.encode_canonical().unwrap(), second.encode_canonical().unwrap() ); assert_eq!( first.evidence_generation_digest().unwrap(), second.evidence_generation_digest().unwrap() ); let mut lease = SnapshotLeaseOracle::new(); assert!(lease.admit_request()); lease.close_or_expire(); assert_eq!(lease.phase(), SnapshotLeasePhase::Closed); assert!(lease.base_pin_held()); assert!(!lease.admit_request()); assert!(lease.release_request()); assert_eq!(lease.phase(), SnapshotLeasePhase::Released); assert!(!lease.base_pin_held()); // Closing/expiring with zero admitted request pins releases the base // pin immediately, per plan ยง6.2: "released immediately when no admitted // request pin remains". let mut empty_lease = SnapshotLeaseOracle::new(); assert_eq!(empty_lease.request_pins(), 0); empty_lease.close_or_expire(); assert_eq!(empty_lease.phase(), SnapshotLeasePhase::Released); assert!(!empty_lease.base_pin_held()); } #[test] fn signed_private_read_and_missing_object_contracts_detect_tampering() { let signed = signed_read(); roundtrip(&signed); signed.verify().unwrap(); let request = MissingObjectsRequestV1 { generation_digest: id(71), object_ids: vec![id(1), id(2), id(3)], }; roundtrip(&request); roundtrip(&MissingObjectsResponseV1 { generation_digest: request.generation_digest, missing_ids: request.object_ids.clone(), }); let mut unordered = request.clone(); unordered.object_ids.swap(0, 1); assert!(unordered.encode_canonical().is_err()); let mut tampered = signed.clone(); tampered.request.snapshot_token_digest = id(72); assert!(tampered.verify().is_err()); let mut wrong_method = signed.request; wrong_method.method = HttpMethodV2::Get; assert!(wrong_method.encode_canonical().is_err()); } #[test] fn every_read_route_method_pairing_is_signed_and_frozen() { let reads = signed_reads(); let expected_methods = [ HttpMethodV2::Get, HttpMethodV2::Get, HttpMethodV2::Post, HttpMethodV2::Post, HttpMethodV2::Put, HttpMethodV2::Delete, ]; assert_eq!(reads.len(), expected_methods.len()); for (read, expected_method) in reads.iter().zip(expected_methods) { roundtrip(read); read.verify().unwrap(); assert_eq!(read.request.method, expected_method); } assert!(reads[0].request.snapshot_token_digest.is_zero()); assert!(!reads[1].request.snapshot_token_digest.is_zero()); } #[test] fn evidence_variants_are_canonical_and_bind_identity_epoch_and_admin_context() { let evidence = all_evidence(); let expected_kinds = [ SourceKindV1::Client, SourceKindV1::Client, SourceKindV1::MirrorEvent, SourceKindV1::MirrorSnapshot, SourceKindV1::LegacyMigration, SourceKindV1::ProjectionAdmin, SourceKindV1::Administrative, ]; for (value, expected_kind) in evidence.iter().zip(expected_kinds) { assert_eq!(value.source_kind(), expected_kind); roundtrip(value); value .verify_authenticated_binding(evidence_context_for(value)) .unwrap(); } for value in &evidence[4..] { let wrong_context = AdministrativeEvidenceContextV1 { destination_repo: id(99), ..evidence_context_for(value) }; assert!(value.verify_authenticated_binding(wrong_context).is_err()); } let legacy = &evidence[4]; let original_context = evidence_context_for(legacy); let mut ordinal_substitution = legacy.clone(); if let TransactionEvidenceV1::LegacyMigrationV1 { chunk_ordinal, .. } = &mut ordinal_substitution { *chunk_ordinal += 1; } let changed_context = evidence_context_for(&ordinal_substitution); assert_ne!(original_context.operation_id, changed_context.operation_id); assert!(ordinal_substitution .verify_authenticated_binding(changed_context) .is_err()); let mut wrong_epoch = evidence[2].clone(); if let TransactionEvidenceV1::MirrorEventV1 { source_key_epoch, .. } = &mut wrong_epoch { *source_key_epoch += 1; } assert!(wrong_epoch .verify_authenticated_binding(evidence_context_for(&wrong_epoch)) .is_err()); let signed_event = signed_event(); signed_event.verify(key(7).public().0).unwrap(); let mut epoch_substitution = signed_event; epoch_substitution.source_key_epoch += 1; assert!(epoch_substitution.verify(key(7).public().0).is_err()); } #[test] fn event_hash_chain_and_dual_sequence_domains_are_independent() { let transaction = committed_transaction(); roundtrip(&transaction); let page = TransactionPageV1 { repo_id: transaction.repo_id, after_repo_sequence: 11, after_event_digest: id(39), fixed_upper_repo_sequence: 12, events: vec![signed_event()], }; roundtrip(&page); page.verify(key(7).public().0).unwrap(); let mut wrong_upper = page; wrong_upper.fixed_upper_repo_sequence = 11; assert!(wrong_upper.encode_canonical().is_err()); let relation = SequenceRelationV1 { previous_shard_sequence: 100, next_shard_sequence: 101, previous_repo_sequence: Some(11), previous_event_digest: Some(id(39)), }; relation.validate_next(&transaction).unwrap(); let other_repo_first = CommittedTransactionV1 { repo_id: id(200), repo_sequence: 1, previous_event_digest: ObjectId([0; 32]), ..transaction.clone() }; SequenceRelationV1 { previous_shard_sequence: 101, next_shard_sequence: 102, previous_repo_sequence: None, previous_event_digest: None, } .validate_next(&other_repo_first) .unwrap(); let mut gap = transaction; gap.repo_sequence += 1; assert!(relation.validate_next(&gap).is_err()); } #[test] fn staged_projection_bytes_manifest_and_install_are_one_binding() { let (session, chunks, manifest, install) = staged_projection(); roundtrip(&session); roundtrip(&chunks[0]); roundtrip(&manifest); roundtrip(&install); validate_projection_stage_binding(&session, &chunks, &manifest, &install).unwrap(); let mut changed = chunks.clone(); changed[0].objects[0].raw_bytes.push(0); assert!(changed[0].encode_canonical().is_err()); let mut wrong_install = install; wrong_install.membership_root = id(1); assert!( validate_projection_stage_binding(&session, &chunks, &manifest, &wrong_install).is_err() ); } #[test] fn projection_stage_finalize_rebinds_the_exact_final_operation() { let (session, ..) = staged_projection(); validate_projection_stage_finalize( &session, session.expires_at_micros - 1, session.final_operation_id, session.final_operation_digest, session.final_evidence_digest, session.actor, session.fork_proof.as_ref(), ) .unwrap(); assert!(validate_projection_stage_finalize( &session, session.expires_at_micros + 1, session.final_operation_id, session.final_operation_digest, session.final_evidence_digest, session.actor, session.fork_proof.as_ref(), ) .is_err()); assert!(validate_projection_stage_finalize( &session, session.expires_at_micros - 1, [0x99; 16], session.final_operation_digest, session.final_evidence_digest, session.actor, session.fork_proof.as_ref(), ) .is_err()); assert!(validate_projection_stage_finalize( &session, session.expires_at_micros - 1, session.final_operation_id, session.final_operation_digest, session.final_evidence_digest, key(9).public().0, session.fork_proof.as_ref(), ) .is_err()); let fork_proof = ForkProofV2 { source_repo_id: id(60), source_genesis: id(61), source_tip: id(62), source_authority: id(63), }; let fork_session = ProjectionStageSessionV1 { source_kind: StageSourceKindV1::Fork, fork_proof: Some(fork_proof.clone()), ..session }; assert!(validate_projection_stage_finalize( &fork_session, fork_session.expires_at_micros - 1, fork_session.final_operation_id, fork_session.final_operation_digest, fork_session.final_evidence_digest, fork_session.actor, None, ) .is_err()); validate_projection_stage_finalize( &fork_session, fork_session.expires_at_micros - 1, fork_session.final_operation_id, fork_session.final_operation_digest, fork_session.final_evidence_digest, fork_session.actor, Some(&fork_proof), ) .unwrap(); } #[test] fn push_and_init_http_body_framing_is_length_prefixed_and_exact() { let signed_push = signed_normal_push(); let pack_bytes = vec![0x41; signed_push.operation.pack_len as usize]; let body = encode_push_request_body(&signed_push, &pack_bytes).unwrap(); let (decoded, decoded_pack) = decode_push_request_body(&body).unwrap(); assert_eq!(decoded, signed_push); assert_eq!(decoded_pack, pack_bytes.as_slice()); let mut truncated = body.clone(); truncated.pop(); assert!(decode_push_request_body(&truncated).is_err()); let mut extended = body.clone(); extended.push(0); assert!(decode_push_request_body(&extended).is_err()); assert!(encode_push_request_body(&signed_push, &pack_bytes[..pack_bytes.len() - 1]).is_err()); let signed_init_op = signed_init(); let genesis_bytes = vec![0x42; signed_init_op.operation.genesis_len as usize]; let init_body = encode_init_request_body(&signed_init_op, &genesis_bytes).unwrap(); let (decoded_init, decoded_genesis) = decode_init_request_body(&init_body).unwrap(); assert_eq!(decoded_init, signed_init_op); assert_eq!(decoded_genesis, genesis_bytes.as_slice()); let mut truncated_init = init_body.clone(); truncated_init.pop(); assert!(decode_init_request_body(&truncated_init).is_err()); assert!( encode_init_request_body(&signed_init_op, &genesis_bytes[..genesis_bytes.len() - 1]) .is_err() ); } #[test] fn projection_oracle_freezes_every_reduced_boundary() { use ProjectionDecisionV1::*; use ProjectionEdgeV1::*; assert_eq!( projection_decision(ProjectionMode::Full, ForeignForkClosure), RetainAndVerify ); assert_eq!( projection_decision(ProjectionMode::Release, BranchRef), OmitNonMember ); assert_eq!( projection_decision(ProjectionMode::Release, OrdinaryCommitParent), VerifyThenOmitBoundary ); assert_eq!( projection_decision( ProjectionMode::Release, ImmediatePredecessorEnvelope { tree_matches_release: true } ), RetainAndVerify ); assert_eq!( projection_decision( ProjectionMode::Release, ImmediatePredecessorEnvelope { tree_matches_release: false } ), Reject ); assert_eq!( projection_decision(ProjectionMode::Metadata, ReleaseTreeOrBlob), VerifyThenOmitBoundary ); assert_eq!( projection_decision(ProjectionMode::Metadata, SignedTransactionEvidence), RetainAndVerify ); } #[test] fn cursor_expiry_forces_authenticated_resnapshot_and_cutover_is_strict() { let digest = snapshot().snapshot.generation_digest().unwrap(); let expired = cursor_disposition(9, 10, 12, digest).unwrap(); let CursorDispositionV1::Resnapshot(expired) = expired else { panic!("cursor below floor must resnapshot"); }; roundtrip(&expired); assert_eq!(expired.authenticated_snapshot_digest, digest); assert_eq!( cursor_disposition(10, 10, 12, digest).unwrap(), CursorDispositionV1::ReplayFrom { after: 10, through: 12 } ); assert!(cursor_disposition(13, 10, 12, digest).is_err()); assert!(matches!( object_dependency_disposition(8, 9, 12, digest).unwrap(), ObjectDependencyDispositionV1::Resnapshot { .. } )); assert_eq!( object_dependency_disposition(9, 9, 12, digest).unwrap(), ObjectDependencyDispositionV1::Available ); let cutover = MaintenanceCutoverV1 { storage_format_version: 2, minimum_protocol_version: 2, v1_post_disabled: true, writer_set_digest: id(1), peer_set_digest: id(2), maintenance_generation: 1, }; roundtrip(&cutover); let mut unsafe_cutover = cutover; unsafe_cutover.v1_post_disabled = false; assert!(unsafe_cutover.encode_canonical().is_err()); } #[test] fn authority_successor_objects_wait_for_the_next_transaction() { let valid = AuthorityTransitionFactsV1 { expected_authority: id(1), authority_update: Some(id(2)), boundary_commit_count: 1, boundary_commit_cites_expected: true, boundary_exposes_direct_successor: true, cas_publishes_with_boundary: true, unrelated_successor_commit_or_release_count: 0, successor_reference_outside_boundary_path_count: 0, }; valid.validate_normal_push().unwrap(); let mut same_transaction_successor = valid.clone(); same_transaction_successor.unrelated_successor_commit_or_release_count = 1; assert!(same_transaction_successor.validate_normal_push().is_err()); let later_transaction = AuthorityTransitionFactsV1 { expected_authority: id(2), authority_update: None, boundary_commit_count: 0, boundary_commit_cites_expected: false, boundary_exposes_direct_successor: false, cas_publishes_with_boundary: false, unrelated_successor_commit_or_release_count: 0, successor_reference_outside_boundary_path_count: 0, }; later_transaction.validate_normal_push().unwrap(); } #[test] fn foreign_fork_boundary_is_native_only_at_the_destination_commit() { let valid = ForeignForkBoundaryFactsV1 { destination_repo_id: id(1), destination_genesis: id(2), destination_current_authority: id(2), destination_is_empty: true, source_repo_id: id(3), derived_source_repo_id: id(3), source_genesis: id(4), verified_source_genesis: id(4), source_tip: id(5), fork_parent: id(5), source_authority: id(6), parent_cited_authority: id(6), fork_commit_authority: id(2), fork_tree_authority: id(2), fork_parent_count: 1, fork_and_modifies_authority_flags: true, envelope_signer_is_destination_owner: true, envelope_signer_is_commit_signer: true, source_read_authorized: true, projection_proof_complete: true, }; valid.validate().unwrap(); let mut forged_genesis = valid.clone(); forged_genesis.verified_source_genesis = id(99); assert!(forged_genesis.validate().is_err()); let mut wrong_destination = valid.clone(); wrong_destination.fork_commit_authority = valid.source_authority; assert!(wrong_destination.validate().is_err()); let mut nonempty = valid.clone(); nonempty.destination_is_empty = false; assert!(nonempty.validate().is_err()); let mut private_unauthorized = valid.clone(); private_unauthorized.source_read_authorized = false; assert!(private_unauthorized.validate().is_err()); let mut missing_projection = valid; missing_projection.projection_proof_complete = false; assert!(missing_projection.validate().is_err()); } #[test] fn deterministic_object_ref_snapshot_event_evidence_and_status_digests_exist() { let blob = Blob::new(vec![0x5a; 1_024]); let object_digest = blob.object_id(); assert_eq!(object_digest, levcs_core::blake3_hash(&blob.serialize())); let snapshot = snapshot(); let refs_digest = ref_state_digest(&snapshot.snapshot.refs).unwrap(); let snapshot_digest = snapshot.snapshot.generation_digest().unwrap(); let event_digest = committed_transaction().event_digest().unwrap(); let evidence_digest = all_evidence()[3].evidence_digest().unwrap(); let status_digest = TransactionStatusV1::Committed(receipt()) .status_digest() .unwrap(); for digest in [ object_digest, refs_digest, snapshot_digest, event_digest, evidence_digest, status_digest, ] { assert!(!digest.is_zero()); } }