lib

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

commit 2481d41fe0eb1e585aa54a99dbeb9d4d0d790264
parent 1a6d311cc2a51aad9d1b4fb74b841e729f6490d5
Author: triesap <tyson@radroots.org>
Date:   Tue, 28 Jul 2026 12:25:25 +0000

event_store: cover protocol reconciliation seals

- compare complete post-extension authority snapshots
- reject isolated marker and source high-water drift
- validate global and active transition bounds
- cover malformed raw-head candidate mapping

Diffstat:
Mcontracts/event_store_production_sources.toml | 2+-
Mcrates/event_store/src/store/protocol_reconciliation_v1.rs | 310++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
2 files changed, 181 insertions(+), 131 deletions(-)

diff --git a/contracts/event_store_production_sources.toml b/contracts/event_store_production_sources.toml @@ -107,7 +107,7 @@ sha256 = "5ebd237e8f475184480938472e472ff8f4a7ea7b9f46f9d6100520d1c2d643c4" [[sources]] path = "crates/event_store/src/store/protocol_reconciliation_v1.rs" -sha256 = "f9d6e28251c8ecaa60ec6ea1b4c2ab2bc0b9d125e67ea4849d35e09324a20108" +sha256 = "7ec2486d535128c4230ec11664b5996203a483bf3a786ecba2594fd6eb2b244f" [[sources]] path = "crates/event_store/src/store/protocol_storage_v1.rs" diff --git a/crates/event_store/src/store/protocol_reconciliation_v1.rs b/crates/event_store/src/store/protocol_reconciliation_v1.rs @@ -200,18 +200,11 @@ async fn read_protocol_post_extension_authority_seal( )); } - let source_rows = sqlx::query( + let source_row = sqlx::query( "SELECT state.active_generation, state.raw_event_count, state.raw_tag_count, state.raw_high_water_seq, state.last_transition_seq, generation.generation_ordinal, generation.reconciliation_version, generation.addressable_feed_version, generation.event_contract_registry_version, generation.hook_id, generation.hook_manifest_sha256, generation.transition_floor_seq, generation.baseline_raw_event_count, generation.baseline_raw_tag_count, generation.baseline_raw_high_water_seq FROM radroots_event_store_source_state AS state JOIN radroots_event_store_source_generation AS generation ON generation.source_generation = state.active_generation WHERE state.singleton = 1", ) - .fetch_all(&mut **tx) + .fetch_one(&mut **tx) .await?; - if source_rows.len() != 1 { - return protocol_post_extension_drift(format!( - "expected one active source state, found {}", - source_rows.len() - )); - } - let source_row = &source_rows[0]; let source_generation = generation_from_blob(source_row.try_get("active_generation")?)?; let raw_event_count: i64 = source_row.try_get("raw_event_count")?; let raw_tag_count: i64 = source_row.try_get("raw_tag_count")?; @@ -237,9 +230,11 @@ async fn read_protocol_post_extension_authority_seal( .await?; let expected_global_min = (last_transition_seq > 0).then_some(1); let expected_global_max = (last_transition_seq > 0).then_some(last_transition_seq); - if last_transition_seq < 0 - || global_transition_min_seq != expected_global_min - || global_transition_max_seq != expected_global_max + if ( + last_transition_seq < 0, + global_transition_min_seq, + global_transition_max_seq, + ) != (false, expected_global_min, expected_global_max) { return protocol_post_extension_drift(format!( "global transition bounds disagree with source state: min={global_transition_min_seq:?}, max={global_transition_max_seq:?}, last={last_transition_seq}" @@ -258,28 +253,20 @@ async fn read_protocol_post_extension_authority_seal( .bind(source_generation.as_bytes().as_slice()) .fetch_optional(&mut **tx) .await?; - let active_transition_span = last_transition_seq - .checked_sub(transition_floor_seq) - .ok_or_else(|| RadrootsEventStoreError::MigrationHookStateDrift { - hook_id: "nip09_reconciliation_v1", - reason: format!( - "active transition floor exceeds source authority: floor={transition_floor_seq}, last={last_transition_seq}" - ), - })?; - let expected_active_min = if active_transition_span == 0 { + // Both persisted values are nonnegative SQLite INTEGERs, so their + // difference remains representable even when the authority is corrupt. + let active_transition_span = last_transition_seq - transition_floor_seq; + let expected_active_min = if active_transition_span <= 0 { None } else { - Some(transition_floor_seq.checked_add(1).ok_or_else(|| { - RadrootsEventStoreError::MigrationHookStateDrift { - hook_id: "nip09_reconciliation_v1", - reason: "active transition floor is exhausted at SQLite INTEGER maximum".to_owned(), - } - })?) + Some(transition_floor_seq + 1) }; let expected_active_max = (active_transition_span > 0).then_some(last_transition_seq); - if active_transition_span < 0 - || active_transition_min_seq != expected_active_min - || active_transition_max_seq != expected_active_max + if ( + active_transition_span < 0, + active_transition_min_seq, + active_transition_max_seq, + ) != (false, expected_active_min, expected_active_max) { return protocol_post_extension_drift(format!( "active transition bounds disagree with source state: floor={transition_floor_seq}, last={last_transition_seq}, min={active_transition_min_seq:?}, max={active_transition_max_seq:?}" @@ -334,89 +321,73 @@ fn protocol_post_extension_authority_matches( expected: &ProtocolPostExtensionAuthoritySeal, actual: &ProtocolPostExtensionAuthoritySeal, ) -> bool { - let ProtocolPostExtensionAuthoritySeal { - source_generation: expected_source_generation, - generation_ordinal: expected_generation_ordinal, - reconciliation_version: expected_reconciliation_version, - addressable_feed_version: expected_addressable_feed_version, - event_contract_registry_version: expected_event_contract_registry_version, - hook_id: expected_hook_id, - hook_manifest_sha256: expected_hook_manifest_sha256, - transition_floor_seq: expected_transition_floor_seq, - baseline_raw_event_count: expected_baseline_raw_event_count, - baseline_raw_tag_count: expected_baseline_raw_tag_count, - baseline_raw_high_water_seq: expected_baseline_raw_high_water_seq, - raw_event_count: expected_raw_event_count, - raw_tag_count: expected_raw_tag_count, - raw_event_bytes: expected_raw_event_bytes, - raw_tag_bytes: expected_raw_tag_bytes, - raw_high_water_seq: expected_raw_high_water_seq, - last_transition_seq: expected_last_transition_seq, - retained_generation_count: expected_retained_generation_count, - retained_generation_limit: expected_retained_generation_limit, - actual_raw_high_water_seq: expected_actual_raw_high_water_seq, - global_transition_min_seq: expected_global_transition_min_seq, - global_transition_max_seq: expected_global_transition_max_seq, - active_transition_min_seq: expected_active_transition_min_seq, - active_transition_max_seq: expected_active_transition_max_seq, - main_schema_version: expected_main_schema_version, - temp_schema_version: expected_temp_schema_version, - } = expected; - let ProtocolPostExtensionAuthoritySeal { - source_generation: actual_source_generation, - generation_ordinal: actual_generation_ordinal, - reconciliation_version: actual_reconciliation_version, - addressable_feed_version: actual_addressable_feed_version, - event_contract_registry_version: actual_event_contract_registry_version, - hook_id: actual_hook_id, - hook_manifest_sha256: actual_hook_manifest_sha256, - transition_floor_seq: actual_transition_floor_seq, - baseline_raw_event_count: actual_baseline_raw_event_count, - baseline_raw_tag_count: actual_baseline_raw_tag_count, - baseline_raw_high_water_seq: actual_baseline_raw_high_water_seq, - raw_event_count: actual_state_raw_event_count, - raw_tag_count: actual_state_raw_tag_count, - raw_event_bytes: actual_raw_event_bytes, - raw_tag_bytes: actual_raw_tag_bytes, - raw_high_water_seq: actual_state_raw_high_water_seq, - last_transition_seq: actual_last_transition_seq, - retained_generation_count: actual_retained_generation_count, - retained_generation_limit: actual_retained_generation_limit, - actual_raw_high_water_seq: actual_observed_raw_high_water_seq, - global_transition_min_seq: actual_global_transition_min_seq, - global_transition_max_seq: actual_global_transition_max_seq, - active_transition_min_seq: actual_active_transition_min_seq, - active_transition_max_seq: actual_active_transition_max_seq, - main_schema_version: actual_main_schema_version, - temp_schema_version: actual_temp_schema_version, - } = actual; - - expected_source_generation == actual_source_generation - && expected_generation_ordinal == actual_generation_ordinal - && expected_reconciliation_version == actual_reconciliation_version - && expected_addressable_feed_version == actual_addressable_feed_version - && expected_event_contract_registry_version == actual_event_contract_registry_version - && expected_hook_id == actual_hook_id - && expected_hook_manifest_sha256 == actual_hook_manifest_sha256 - && expected_transition_floor_seq == actual_transition_floor_seq - && expected_baseline_raw_event_count == actual_baseline_raw_event_count - && expected_baseline_raw_tag_count == actual_baseline_raw_tag_count - && expected_baseline_raw_high_water_seq == actual_baseline_raw_high_water_seq - && expected_raw_event_count == actual_state_raw_event_count - && expected_raw_tag_count == actual_state_raw_tag_count - && expected_raw_event_bytes == actual_raw_event_bytes - && expected_raw_tag_bytes == actual_raw_tag_bytes - && expected_raw_high_water_seq == actual_state_raw_high_water_seq - && expected_last_transition_seq == actual_last_transition_seq - && expected_retained_generation_count == actual_retained_generation_count - && expected_retained_generation_limit == actual_retained_generation_limit - && expected_actual_raw_high_water_seq == actual_observed_raw_high_water_seq - && expected_global_transition_min_seq == actual_global_transition_min_seq - && expected_global_transition_max_seq == actual_global_transition_max_seq - && expected_active_transition_min_seq == actual_active_transition_min_seq - && expected_active_transition_max_seq == actual_active_transition_max_seq - && expected_main_schema_version == actual_main_schema_version - && expected_temp_schema_version == actual_temp_schema_version + ( + ( + &expected.source_generation, + expected.generation_ordinal, + expected.reconciliation_version, + expected.addressable_feed_version, + expected.event_contract_registry_version, + &expected.hook_id, + &expected.hook_manifest_sha256, + expected.transition_floor_seq, + expected.baseline_raw_event_count, + ), + ( + expected.baseline_raw_tag_count, + expected.baseline_raw_high_water_seq, + expected.raw_event_count, + expected.raw_tag_count, + expected.raw_event_bytes, + expected.raw_tag_bytes, + expected.raw_high_water_seq, + expected.last_transition_seq, + expected.retained_generation_count, + ), + ( + expected.retained_generation_limit, + expected.actual_raw_high_water_seq, + expected.global_transition_min_seq, + expected.global_transition_max_seq, + expected.active_transition_min_seq, + expected.active_transition_max_seq, + expected.main_schema_version, + expected.temp_schema_version, + ), + ) == ( + ( + &actual.source_generation, + actual.generation_ordinal, + actual.reconciliation_version, + actual.addressable_feed_version, + actual.event_contract_registry_version, + &actual.hook_id, + &actual.hook_manifest_sha256, + actual.transition_floor_seq, + actual.baseline_raw_event_count, + ), + ( + actual.baseline_raw_tag_count, + actual.baseline_raw_high_water_seq, + actual.raw_event_count, + actual.raw_tag_count, + actual.raw_event_bytes, + actual.raw_tag_bytes, + actual.raw_high_water_seq, + actual.last_transition_seq, + actual.retained_generation_count, + ), + ( + actual.retained_generation_limit, + actual.actual_raw_high_water_seq, + actual.global_transition_min_seq, + actual.global_transition_max_seq, + actual.active_transition_min_seq, + actual.active_transition_max_seq, + actual.main_schema_version, + actual.temp_schema_version, + ), + ) } fn protocol_post_extension_drift<T>(reason: String) -> Result<T, RadrootsEventStoreError> { @@ -571,23 +542,9 @@ pub(super) async fn apply_raw_event_head( let candidate = match profile { ReconciliationProfile::Nip09V1RegistryV7 => event_head_candidate_for_nip01_event_v1(event), }; - let candidate = match candidate { - RadrootsEventHeadCandidateResult::Candidate(candidate) => candidate, - RadrootsEventHeadCandidateResult::NotHeadSelected => { - return Ok(AppliedHead { - decision: RadrootsRawHeadDecision::NotHeadSelected, - }); - } - RadrootsEventHeadCandidateResult::NotPersisted => { - return Ok(AppliedHead { - decision: RadrootsRawHeadDecision::NotPersisted, - }); - } - RadrootsEventHeadCandidateResult::Malformed(_) => { - return Ok(AppliedHead { - decision: RadrootsRawHeadDecision::MalformedCoordinate, - }); - } + let candidate = match raw_head_candidate(candidate) { + Ok(candidate) => candidate, + Err(decision) => return Ok(AppliedHead { decision }), }; let current = current_event_head(tx, &candidate.coordinate).await?; let protocol_decision = match profile { @@ -603,6 +560,23 @@ pub(super) async fn apply_raw_event_head( }) } +fn raw_head_candidate( + candidate: RadrootsEventHeadCandidateResult, +) -> Result<RadrootsEventHeadCandidate, RadrootsRawHeadDecision> { + match candidate { + RadrootsEventHeadCandidateResult::Candidate(candidate) => Ok(candidate), + RadrootsEventHeadCandidateResult::NotHeadSelected => { + Err(RadrootsRawHeadDecision::NotHeadSelected) + } + RadrootsEventHeadCandidateResult::NotPersisted => { + Err(RadrootsRawHeadDecision::NotPersisted) + } + RadrootsEventHeadCandidateResult::Malformed(_) => { + Err(RadrootsRawHeadDecision::MalformedCoordinate) + } + } +} + async fn current_event_head( tx: &mut Transaction<'_, Sqlite>, coordinate: &RadrootsEventHeadCoordinate, @@ -690,6 +664,26 @@ const fn bool_i64(value: bool) -> i64 { mod tests { use super::*; + async fn protocol_seal_error_after_corruption( + statements: &[&'static str], + ) -> RadrootsEventStoreError { + let store = crate::RadrootsEventStore::open_memory() + .await + .expect("open store"); + let mut transaction = store.pool().begin().await.expect("transaction"); + for statement in statements { + sqlx::query(*statement) + .execute(&mut *transaction) + .await + .expect("apply trusted corruption"); + } + let error = read_protocol_post_extension_authority_seal(&mut transaction) + .await + .expect_err("protocol seal must reject corruption"); + transaction.rollback().await.expect("rollback corruption"); + error + } + fn authority_seal() -> ProtocolPostExtensionAuthoritySeal { ProtocolPostExtensionAuthoritySeal { source_generation: RadrootsEventStoreSourceGeneration::from_bytes([0x11; 32]), @@ -791,6 +785,62 @@ mod tests { reason, }) if reason == "fixture" )); + assert_eq!( + raw_head_candidate(RadrootsEventHeadCandidateResult::Malformed( + radroots_event::event_head::v1::RadrootsEventHeadMalformed::MissingDTag, + )) + .expect_err("malformed coordinate"), + RadrootsRawHeadDecision::MalformedCoordinate + ); + } + + #[tokio::test] + async fn protocol_authority_seal_rejects_isolated_storage_drift() { + let marker = protocol_seal_error_after_corruption(&[ + "DROP TRIGGER radroots_event_store_source_rebuild_marker_insert_guard", + "INSERT INTO radroots_event_store_source_rebuild_marker(singleton, barrier_key, target_generation, target_generation_ordinal, reconciliation_version, addressable_feed_version, event_contract_registry_version, hook_id, hook_manifest_sha256, transition_floor_seq, baseline_raw_event_count, baseline_raw_tag_count, baseline_raw_high_water_seq, prior_active_generation, prior_raw_event_count, prior_raw_tag_count, prior_raw_high_water_seq, prior_last_transition_seq) SELECT 1, 1, generation.source_generation, generation.generation_ordinal, generation.reconciliation_version, generation.addressable_feed_version, generation.event_contract_registry_version, generation.hook_id, generation.hook_manifest_sha256, generation.transition_floor_seq, state.raw_event_count, state.raw_tag_count, state.raw_high_water_seq, state.active_generation, state.raw_event_count, state.raw_tag_count, state.raw_high_water_seq, state.last_transition_seq FROM radroots_event_store_source_generation AS generation JOIN radroots_event_store_source_state AS state ON state.active_generation = generation.source_generation WHERE state.singleton = 1", + ]) + .await; + assert!(matches!( + marker, + RadrootsEventStoreError::MigrationHookStateDrift { reason, .. } + if reason.contains("source rebuild marker residue") + )); + + let raw_high_water = protocol_seal_error_after_corruption(&[ + "DROP TRIGGER radroots_event_store_source_capacity_update_guard", + "DROP TRIGGER radroots_event_store_source_state_authority_update_guard", + "UPDATE radroots_event_store_source_capacity_v1 SET raw_event_count = 1, raw_high_water_seq = 1 WHERE singleton = 1", + "UPDATE radroots_event_store_source_state SET raw_event_count = 1, raw_high_water_seq = 1 WHERE singleton = 1", + ]) + .await; + assert!(matches!( + raw_high_water, + RadrootsEventStoreError::MigrationHookStateDrift { reason, .. } + if reason.contains("raw high-water does not match source authority") + )); + + let global_bounds = protocol_seal_error_after_corruption(&[ + "DROP TRIGGER radroots_event_store_source_state_authority_update_guard", + "UPDATE radroots_event_store_source_state SET last_transition_seq = 1 WHERE singleton = 1", + ]) + .await; + assert!(matches!( + global_bounds, + RadrootsEventStoreError::MigrationHookStateDrift { reason, .. } + if reason.contains("global transition bounds disagree") + )); + + let active_bounds = protocol_seal_error_after_corruption(&[ + "DROP TRIGGER radroots_event_store_source_generation_update_guard", + "UPDATE radroots_event_store_source_generation SET transition_floor_seq = 1 WHERE source_generation = (SELECT active_generation FROM radroots_event_store_source_state WHERE singleton = 1)", + ]) + .await; + assert!(matches!( + active_bounds, + RadrootsEventStoreError::MigrationHookStateDrift { reason, .. } + if reason.contains("active transition bounds disagree") + )); } #[tokio::test]