lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit fb687f02c56e331c5528871ceab399c689811d0f
parent 5fb29ccaacdbe076dd831163a7461cfd5aab8e2e
Author: triesap <tyson@radroots.org>
Date:   Tue, 28 Jul 2026 09:09:34 +0000

event_store: cover transition lineage authority

- reject incoherent applied and non-head transition causes
- verify deletion author and coordinate targeting authority
- detect repeated, missing, and baseline retraction lineage
- exercise every failure through the public transition feed

Diffstat:
Mcrates/event_store/src/store.rs | 181+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 181 insertions(+), 0 deletions(-)

diff --git a/crates/event_store/src/store.rs b/crates/event_store/src/store.rs @@ -2587,6 +2587,100 @@ mod tests { (store, food_id) } + async fn transition_cause_corruption_store() -> RadrootsEventStore { + let store = RadrootsEventStore::open_memory().await.expect("open"); + let regular = signed_event(KIND_POST, 240, Vec::new(), "transition cause fixture"); + let food = food_availability_event( + 250, + "transition-cause-carrots", + "Transition Cause Carrots", + "Transition cause harvest", + "active", + Vec::new(), + ); + for (observed_at_ms, event) in [(19_300, regular), (19_301, food)] { + store + .ingest_event(RadrootsEventIngest::new(event, observed_at_ms)) + .await + .expect("transition cause fixture ingest"); + } + store + } + + async fn replacement_transition_corruption_store() -> RadrootsEventStore { + let store = RadrootsEventStore::open_memory().await.expect("open"); + let first = food_availability_event( + 260, + "transition-lineage-carrots", + "Transition Lineage Carrots", + "First lineage harvest", + "active", + Vec::new(), + ); + let second = food_availability_event( + 270, + "transition-lineage-carrots", + "Transition Lineage Carrots", + "Second lineage harvest", + "sold", + Vec::new(), + ); + for (observed_at_ms, event) in [(19_400, first), (19_401, second)] { + store + .ingest_event(RadrootsEventIngest::new(event, observed_at_ms)) + .await + .expect("replacement transition fixture ingest"); + } + store + } + + async fn transition_feed_error_after_trusted_corruption( + store: &RadrootsEventStore, + additional_guards: &[&'static str], + mutations: &[&'static str], + ) -> RadrootsEventStoreError { + let mut connection = store.pool().acquire().await.expect("trusted connection"); + sqlx::query("DROP TRIGGER radroots_event_store_addressable_transition_update_guard") + .execute(&mut *connection) + .await + .expect("trusted transition guard removal"); + for guard in additional_guards { + sqlx::query(*guard) + .execute(&mut *connection) + .await + .expect("trusted transition dependency guard removal"); + } + sqlx::query("PRAGMA foreign_keys = OFF") + .execute(&mut *connection) + .await + .expect("disable trusted foreign-key enforcement"); + sqlx::query("PRAGMA ignore_check_constraints = ON") + .execute(&mut *connection) + .await + .expect("enable trusted check-constraint bypass"); + for mutation in mutations { + sqlx::query(*mutation) + .execute(&mut *connection) + .await + .expect("trusted transition lineage corruption"); + } + sqlx::query("PRAGMA ignore_check_constraints = OFF") + .execute(&mut *connection) + .await + .expect("restore check-constraint enforcement"); + sqlx::query("PRAGMA foreign_keys = ON") + .execute(&mut *connection) + .await + .expect("restore foreign-key enforcement"); + drop(connection); + + let scope = crate::RadrootsAddressableTransitionScopeV1::food_availability(); + store + .addressable_transition_page_v1(&scope, None, 64) + .await + .expect_err("corrupt transition lineage must fail public feed read") + } + fn calendar_date_event( created_at: u32, d_tag: &str, @@ -9403,6 +9497,93 @@ CREATE TABLE aux.event_transport_observation (event_id TEXT);", } #[tokio::test] + async fn addressable_transition_feed_rejects_incremental_cause_and_lineage_drift() { + for (label, mutation) in [ + ( + "applied cause identity", + "UPDATE radroots_event_store_addressable_head_transition SET cause_event_id = (SELECT event_id FROM event_envelopes WHERE kind = 1), cause_event_seq = (SELECT seq FROM event_envelopes WHERE kind = 1)", + ), + ( + "non-deletion non-head cause", + "UPDATE radroots_event_store_addressable_head_transition SET raw_head_decision = 'not_head_selected'", + ), + ( + "illegal incremental decision", + "UPDATE radroots_event_store_addressable_head_transition SET raw_head_decision = 'skipped_older'", + ), + ] { + let store = transition_cause_corruption_store().await; + let error = + transition_feed_error_after_trusted_corruption(&store, &[], &[mutation]).await; + assert!( + matches!( + error, + RadrootsEventStoreError::AddressableTransitionCorruption { .. } + ), + "{label}: {error}", + ); + } + + let (evidence_store, _) = suppressed_food_visibility_store().await; + let evidence_error = transition_feed_error_after_trusted_corruption( + &evidence_store, + &[], + &[ + "UPDATE radroots_event_store_addressable_head_transition SET visibility = 'visible', visible_event_id = raw_head_event_id, visible_event_seq = raw_head_event_seq, retracted_event_id = NULL, retracted_event_seq = NULL, nip09_outcome = 'visible', nip09_reason = 'deletion_request_author_mismatch', event_reference_request_id = NULL, address_reference_request_id = NULL, address_reference_cutoff = NULL WHERE transition_seq = 2", + ], + ) + .await; + assert!(matches!( + evidence_error, + RadrootsEventStoreError::AddressableTransitionCorruption { ref reason } + if reason.contains("author does not agree") + )); + + let (target_store, _) = suppressed_food_visibility_store().await; + let target_error = transition_feed_error_after_trusted_corruption( + &target_store, + &["DROP TRIGGER radroots_event_store_nip09_address_target_delete_guard"], + &["DELETE FROM radroots_event_store_nip09_address_target"], + ) + .await; + assert!(matches!( + target_error, + RadrootsEventStoreError::AddressableTransitionCorruption { ref reason } + if reason.contains("does not target the transitioned coordinate") + )); + + for (label, mutation, expected_reason) in [ + ( + "missing retraction", + "UPDATE radroots_event_store_addressable_head_transition SET retracted_event_id = NULL, retracted_event_seq = NULL WHERE transition_seq = 2", + "retraction does not match", + ), + ( + "repeated prior state", + "UPDATE radroots_event_store_addressable_head_transition SET raw_head_event_id = (SELECT raw_head_event_id FROM radroots_event_store_addressable_head_transition WHERE transition_seq = 1), raw_head_event_seq = (SELECT raw_head_event_seq FROM radroots_event_store_addressable_head_transition WHERE transition_seq = 1), raw_head_created_at = (SELECT raw_head_created_at FROM radroots_event_store_addressable_head_transition WHERE transition_seq = 1), visible_event_id = (SELECT visible_event_id FROM radroots_event_store_addressable_head_transition WHERE transition_seq = 1), visible_event_seq = (SELECT visible_event_seq FROM radroots_event_store_addressable_head_transition WHERE transition_seq = 1), retracted_event_id = NULL, retracted_event_seq = NULL, cause_event_id = (SELECT raw_head_event_id FROM radroots_event_store_addressable_head_transition WHERE transition_seq = 1), cause_event_seq = (SELECT raw_head_event_seq FROM radroots_event_store_addressable_head_transition WHERE transition_seq = 1) WHERE transition_seq = 2", + "repeats the complete prior state", + ), + ( + "baseline after prior state", + "UPDATE radroots_event_store_addressable_head_transition SET origin = 'baseline', retracted_event_id = NULL, retracted_event_seq = NULL, cause_event_id = NULL, cause_event_seq = NULL, raw_head_decision = 'baseline_rebuild' WHERE transition_seq = 2", + "baseline transition follows existing coordinate state", + ), + ] { + let store = replacement_transition_corruption_store().await; + let error = + transition_feed_error_after_trusted_corruption(&store, &[], &[mutation]).await; + assert!( + matches!( + error, + RadrootsEventStoreError::AddressableTransitionCorruption { ref reason } + if reason.contains(expected_reason) + ), + "{label}: {error}", + ); + } + } + + #[tokio::test] async fn raw_addressable_heads_use_the_first_opaque_d_value_or_empty() { let store = RadrootsEventStore::open_memory().await.expect("open"); let missing = signed_event(39_990, 30, Vec::new(), "missing");