lib

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

commit 41acfb8af066a1f08e870eb43a09ffed8c428b22
parent b82931ce699202529c35e3a9d21a2be80a533695
Author: triesap <tyson@radroots.org>
Date:   Tue, 28 Jul 2026 11:23:49 +0000

event_store: make transition invariants structural

- bind page arithmetic to governed event wire bounds
- remove duplicate checks after typed and scoped validation
- preserve typed cause keys and validate stored timestamps
- apply production AST authority to immutable predecessors

Diffstat:
Mcontracts/event_store_production_sources.toml | 6+++---
Mcrates/event_store/src/model/addressable_transition_feed_v1.rs | 25++++++++++++-------------
Mcrates/event_store/src/store.rs | 14++++++++++++++
Mcrates/event_store/src/store/addressable_transition_feed_v1.rs | 124+++++++++++++++++++------------------------------------------------------------
Mcrates/event_store/src/store/current_visibility_v1.rs | 17+++++++----------
Mtools/xtask/src/contract/phase1_publication_artifact.rs | 6+++++-
Mtools/xtask/src/contract/raw_source_rebuild.rs | 4++--
7 files changed, 73 insertions(+), 123 deletions(-)

diff --git a/contracts/event_store_production_sources.toml b/contracts/event_store_production_sources.toml @@ -19,7 +19,7 @@ sha256 = "14a98fe4361baa90e74c499ca96cd47822531041dfd874281b1758796b8fd22e" [[sources]] path = "crates/event_store/src/model/addressable_transition_feed_v1.rs" -sha256 = "da82c889de4c131b33f40c88aced0a381b7f213b297d125327334714ab4c2511" +sha256 = "bf0ed43e03af0d45f9406f2ca2463b7ab740b0582191c9ba44a36cdc77de3f4e" [[sources]] path = "crates/event_store/src/model/current_visibility_v1.rs" @@ -71,11 +71,11 @@ sha256 = "ef277328d99ef75a978f2907dc654cbf391436b9db693c3be69655dccbe74f89" [[sources]] path = "crates/event_store/src/store/addressable_transition_feed_v1.rs" -sha256 = "4d1d9b09bacbfc1bcbedcfc641a50679b6b4b51e39786ab9e321a6d78c04f744" +sha256 = "510f9cdeb3756b0f1ae4fcd5c85cb68ff5cb91344bc15cab0aa9c473938d2743" [[sources]] path = "crates/event_store/src/store/current_visibility_v1.rs" -sha256 = "56a21d2ec6a408a2dd5131db0e3c7343c67e60efc67a9c8b2f97fbb90d2d5788" +sha256 = "ba4f77e758d98b25f034cbcc554dda833c839f3ccb8671c0b43997aaac764806" [[sources]] path = "crates/event_store/src/store/food_availability_projection_v1.rs" diff --git a/crates/event_store/src/model/addressable_transition_feed_v1.rs b/crates/event_store/src/model/addressable_transition_feed_v1.rs @@ -17,6 +17,14 @@ pub const RADROOTS_ADDRESSABLE_TRANSITION_D_TAG_MAX_BYTES_V1: usize = radroots_event::wire::v1::DEFAULT_TAG_ELEMENT_MAX_BYTES; pub const RADROOTS_ADDRESSABLE_TRANSITION_CURSOR_JSON_MAX_BYTES_V1: usize = 512; const SCOPE_FINGERPRINT_DOMAIN_V1: &[u8] = b"radroots.addressable-transition-scope.v1\0"; +const _: () = assert!( + radroots_event::wire::v1::DEFAULT_RAW_JSON_MAX_BYTES + <= RADROOTS_ADDRESSABLE_TRANSITION_PAGE_RAW_JSON_MAX_BYTES_V1 +); +const _: () = assert!( + RADROOTS_ADDRESSABLE_TRANSITION_PAGE_LIMIT_MAX_V1 as usize + <= usize::MAX / radroots_event::wire::v1::DEFAULT_RAW_JSON_MAX_BYTES +); #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct RadrootsAddressableTransitionScopeFingerprintV1([u8; 32]); @@ -61,14 +69,6 @@ impl RadrootsAddressableTransitionScopeV1 { if kinds.is_empty() { return Err(RadrootsEventStoreError::AddressableTransitionScopeEmpty); } - if kinds.len() > RADROOTS_ADDRESSABLE_TRANSITION_SCOPE_KIND_MAX_V1 { - return Err( - RadrootsEventStoreError::AddressableTransitionScopeTooLarge { - max: RADROOTS_ADDRESSABLE_TRANSITION_SCOPE_KIND_MAX_V1, - actual: kinds.len(), - }, - ); - } if let Some(kind) = kinds .iter() .copied() @@ -209,11 +209,10 @@ fn decode_cursor_hex( { return Err(RadrootsEventStoreError::AddressableTransitionCursorEncoding { field }); } - let decoded = hex::decode(value) - .map_err(|_| RadrootsEventStoreError::AddressableTransitionCursorEncoding { field })?; - decoded - .try_into() - .map_err(|_| RadrootsEventStoreError::AddressableTransitionCursorEncoding { field }) + let mut decoded = [0_u8; 32]; + hex::decode_to_slice(value, &mut decoded) + .expect("the exact lowercase hexadecimal cursor encoding was validated"); + Ok(decoded) } #[derive(Clone, Copy, Debug, PartialEq, Eq)] diff --git a/crates/event_store/src/store.rs b/crates/event_store/src/store.rs @@ -8803,6 +8803,12 @@ CREATE TABLE aux.event_transport_observation (event_id TEXT);", "stored address deletion cutoff is invalid", ), ( + "negative stored created-at", + true, + "UPDATE radroots_event_store_addressable_head_state SET raw_head_created_at = -1", + "stored raw-head created-at is invalid", + ), + ( "contract disagreement", false, "UPDATE radroots_event_store_addressable_head_state SET contract_id = 'radroots.event.invalid.v1'", @@ -9289,6 +9295,14 @@ CREATE TABLE aux.event_transport_observation (event_id TEXT);", "UPDATE radroots_event_store_addressable_head_transition SET d_tag = replace(hex(zeroblob(513)), '00', 'x')", ), ( + "negative kind", + "UPDATE radroots_event_store_addressable_head_transition SET kind = -1", + ), + ( + "kind above u32", + "UPDATE radroots_event_store_addressable_head_transition SET kind = 4294967296", + ), + ( "raw-head sequence", "UPDATE radroots_event_store_addressable_head_transition SET raw_head_event_seq = 0", ), diff --git a/crates/event_store/src/store/addressable_transition_feed_v1.rs b/crates/event_store/src/store/addressable_transition_feed_v1.rs @@ -70,11 +70,8 @@ pub(super) async fn addressable_transition_page_in_transaction_v1( let mut transitions = Vec::with_capacity(rows.len()); let mut canonical_payload_bytes = 0usize; let mut last_scanned_sequence = start; - let mut stopped_before_row = false; for row in rows { - let transition_seq: i64 = row.try_get("transition_seq").map_err(|error| { - corruption(format!("transition sequence cannot be decoded: {error}")) - })?; + let transition_seq: i64 = row.try_get("transition_seq")?; let expected_sequence = last_scanned_sequence .checked_add(1) .ok_or_else(|| corruption("transition sequence overflow"))?; @@ -85,57 +82,27 @@ pub(super) async fn addressable_transition_page_in_transaction_v1( ), }); } - let kind = u32_from_i64( - "transition.kind", - row.try_get("kind").map_err(|error| { - corruption(format!("transition kind cannot be decoded: {error}")) - })?, - ) - .map_err(|error| corruption(error.to_string()))?; + let kind = u32_from_i64("transition.kind", row.try_get("kind")?) + .map_err(|error| corruption(error.to_string()))?; if !scope.kinds().contains(&kind) { last_scanned_sequence = transition_seq; continue; } if transitions.len() == usize::try_from(limit).expect("u32 fits usize") { - stopped_before_row = true; break; } let transition = transition_from_row(connection, row, source.generation).await?; let transition_payload_bytes = transition .visible_event() .map_or(0, |event| event.raw_json().len()); - let next_payload_bytes = canonical_payload_bytes - .checked_add(transition_payload_bytes) - .ok_or( - RadrootsEventStoreError::AddressableTransitionPagePayloadTooLarge { - max: RADROOTS_ADDRESSABLE_TRANSITION_PAGE_RAW_JSON_MAX_BYTES_V1, - actual: usize::MAX, - }, - )?; + let next_payload_bytes = canonical_payload_bytes + transition_payload_bytes; if next_payload_bytes > RADROOTS_ADDRESSABLE_TRANSITION_PAGE_RAW_JSON_MAX_BYTES_V1 { - if transitions.is_empty() { - return Err( - RadrootsEventStoreError::AddressableTransitionPagePayloadTooLarge { - max: RADROOTS_ADDRESSABLE_TRANSITION_PAGE_RAW_JSON_MAX_BYTES_V1, - actual: next_payload_bytes, - }, - ); - } - stopped_before_row = true; break; } canonical_payload_bytes = next_payload_bytes; last_scanned_sequence = transition_seq; transitions.push(transition); } - if !scan_limited && !stopped_before_row && last_scanned_sequence < source.high_water { - return Err(RadrootsEventStoreError::AddressableTransitionSequenceGap { - reason: format!( - "sealed interval ends at {}, but the last stored transition is {last_scanned_sequence}", - source.high_water - ), - }); - } let has_more = last_scanned_sequence < source.high_water; Ok(RadrootsAddressableTransitionPageV1 { source_high_water: source.high_water, @@ -144,7 +111,8 @@ pub(super) async fn addressable_transition_page_in_transaction_v1( source.generation, scope.fingerprint(), last_scanned_sequence, - )?, + ) + .expect("the validated source interval and cursor preserve a nonnegative sequence"), has_more, }) } @@ -255,14 +223,6 @@ async fn validate_or_create_cursor( let Some(cursor) = cursor else { return Ok(source.floor); }; - if cursor.feed_version() != RADROOTS_ADDRESSABLE_TRANSITION_FEED_VERSION_V1 { - return Err( - RadrootsEventStoreError::AddressableTransitionFeedVersionMismatch { - expected: RADROOTS_ADDRESSABLE_TRANSITION_FEED_VERSION_V1, - actual: cursor.feed_version(), - }, - ); - } if cursor.scope_fingerprint() != scope.fingerprint() { return Err(RadrootsEventStoreError::AddressableTransitionScopeMismatch); } @@ -308,21 +268,9 @@ async fn validate_or_create_cursor( async fn transition_from_row( connection: &mut SqliteConnection, row: sqlx::sqlite::SqliteRow, - expected_generation: RadrootsEventStoreSourceGeneration, + source_generation: RadrootsEventStoreSourceGeneration, ) -> Result<RadrootsAddressableTransitionV1, RadrootsEventStoreError> { let transition_seq: i64 = row.try_get("transition_seq")?; - if transition_seq <= 0 { - return Err(corruption(format!( - "transition sequence {transition_seq} is not positive" - ))); - } - let source_generation = generation_from_blob(row.try_get("source_generation")?) - .map_err(|error| corruption(format!("transition generation is invalid: {error}")))?; - if source_generation != expected_generation { - return Err(corruption( - "scoped query returned a transition from another generation", - )); - } let origin = RadrootsAddressableTransitionOriginV1::parse(row.try_get::<String, _>("origin")?.as_str()) .map_err(|error| corruption(error.to_string()))?; @@ -396,7 +344,8 @@ async fn transition_from_row( raw_head_created_at, )?; - let (raw_event, admission) = load_and_validate_stored_event(connection, &raw_head).await?; + let (raw_event, admission, raw_event_pubkey) = + load_and_validate_stored_event(connection, &raw_head).await?; validate_addressable_reference( connection, source_generation, @@ -415,12 +364,7 @@ async fn transition_from_row( ))); } - let visible_event = if let Some(reference) = visible_reference.as_ref() { - if reference != &raw_head { - return Err(corruption(format!( - "transition {transition_seq} visible event is not the raw head" - ))); - } + let visible_event = if visible_reference.is_some() { Some(RadrootsStoreProducedCanonicalEventV1 { event_id: raw_head.event_id().clone(), pubkey: coordinate.pubkey().clone(), @@ -433,7 +377,7 @@ async fn transition_from_row( }; if let Some(reference) = retracted_event.as_ref() { - let (event, admission) = load_and_validate_stored_event(connection, reference).await?; + let (event, admission, _) = load_and_validate_stored_event(connection, reference).await?; validate_addressable_reference( connection, source_generation, @@ -450,7 +394,11 @@ async fn transition_from_row( } let cause = if let Some(reference) = cause_reference.as_ref() { if reference == &raw_head { - Some((raw_event.clone(), admission.clone())) + Some(( + raw_event.clone(), + admission.clone(), + raw_event_pubkey.clone(), + )) } else { Some(load_and_validate_stored_event(connection, reference).await?) } @@ -488,27 +436,20 @@ async fn transition_from_row( retracted_event.as_ref(), ) .await?; - let cause_event = cause - .map(|(event, admission)| { - let event_reference = cause_reference - .clone() - .ok_or_else(|| corruption("loaded transition cause has no reference"))?; - let pubkey = RadrootsPublicKey::parse(event.pubkey.as_str()).map_err(|error| { - corruption(format!("transition cause pubkey is invalid: {error}")) - })?; - Ok::<RadrootsAddressableTransitionCauseV1, RadrootsEventStoreError>( + let cause_event = + cause + .zip(cause_reference) + .map(|((event, admission, pubkey), event_reference)| { RadrootsAddressableTransitionCauseV1 { - event: event_reference, + event: event_reference.clone(), pubkey, created_at: event.created_at, kind: event.kind, admission_status: admission.status, admission_code: admission.code, contract_id: admission.contract.map(|contract| contract.id.to_owned()), - }, - ) - }) - .transpose()?; + } + }); Ok(RadrootsAddressableTransitionV1 { transition_seq, @@ -548,7 +489,7 @@ async fn validate_incremental_cause( coordinate: &RadrootsAddressableTransitionCoordinateV1, raw_head: &RadrootsAddressableTransitionEventReferenceV1, cause_reference: Option<&RadrootsAddressableTransitionEventReferenceV1>, - cause: Option<&(RadrootsStoredRawEvent, EventAdmission)>, + cause: Option<&(RadrootsStoredRawEvent, EventAdmission, RadrootsPublicKey)>, suppression: Option<&RadrootsNip09SuppressionEvidenceV1>, decision: RadrootsAddressableTransitionRawHeadDecisionV1, ) -> Result<(), RadrootsEventStoreError> { @@ -557,7 +498,7 @@ async fn validate_incremental_cause( } let cause_reference = cause_reference .ok_or_else(|| corruption("incremental transition has no cause reference"))?; - let (cause_event, cause_admission) = + let (cause_event, cause_admission, _) = cause.ok_or_else(|| corruption("incremental transition cause could not be loaded"))?; match decision { RadrootsAddressableTransitionRawHeadDecisionV1::Applied => { @@ -676,9 +617,6 @@ async fn validate_retraction_lineage( || prior_state.raw_head != current.raw_head)) .then_some(prior_state.raw_head) } else { - if origin == RadrootsAddressableTransitionOriginV1::Baseline && retracted.is_some() { - return Err(corruption("baseline transition retracts prior state")); - } None }; if expected.as_ref() != retracted { @@ -857,7 +795,7 @@ fn parse_event_id( async fn load_and_validate_stored_event( connection: &mut SqliteConnection, reference: &RadrootsAddressableTransitionEventReferenceV1, -) -> Result<(RadrootsStoredRawEvent, EventAdmission), RadrootsEventStoreError> { +) -> Result<(RadrootsStoredRawEvent, EventAdmission, RadrootsPublicKey), RadrootsEventStoreError> { let row = sqlx::query( "SELECT seq, event_id, pubkey, created_at, kind, tags_json, content, sig, raw_json, verification_status, contract_status, contract_id, event_class, projection_eligible, inserted_at_ms, updated_at_ms FROM event_envelopes WHERE seq = ? AND event_id = ?", ) @@ -877,11 +815,9 @@ async fn load_and_validate_stored_event( let reconstructed = RadrootsEventIngest::from_raw_json(stored.raw_json.clone(), 0) .map_err(|error| corruption(format!("stored raw event cannot be reverified: {error}")))?; let event = reconstructed.event(); - let tags_json = serde_json::to_string(&event.tags_as_vec()).map_err(|error| { - corruption(format!( - "stored raw event tags cannot be canonicalized: {error}" - )) - })?; + let event_pubkey = event.author().clone(); + let tags_json = serde_json::to_string(&event.tags_as_vec()) + .expect("an in-memory vector of string tags always serializes as JSON"); if stored.event_id != event.id_str() || stored.pubkey != event.author_str() || stored.created_at != event.created_at_u64() @@ -909,7 +845,7 @@ async fn load_and_validate_stored_event( reference.event_id() ))); } - Ok((stored, admission)) + Ok((stored, admission, event_pubkey)) } async fn validate_addressable_reference( diff --git a/crates/event_store/src/store/current_visibility_v1.rs b/crates/event_store/src/store/current_visibility_v1.rs @@ -1,5 +1,5 @@ use super::protocol_storage_v1::stored_raw_event_from_row; -use super::{RadrootsEventStore, bool_from_i64}; +use super::{RadrootsEventStore, bool_from_i64, u64_from_i64}; use crate::RadrootsEventStoreError; use crate::model::{ RadrootsCurrentEventVisibilityV1, RadrootsCurrentVisibilityDecisionV1, @@ -274,6 +274,11 @@ async fn validate_addressable_head_projection( )); }; let evidence = visibility.suppression.as_ref(); + let stored_created_at = u64_from_i64( + "addressable_head_state.raw_head_created_at", + row.try_get("raw_head_created_at")?, + ) + .map_err(|error| visibility_authority_error("stored raw-head created-at", error))?; let stored_cutoff = row .try_get::<Option<i64>, _>("address_reference_cutoff")? .map(|value| { @@ -286,15 +291,7 @@ async fn validate_addressable_head_projection( .map_err(|error| visibility_authority_error("stored address deletion cutoff", error))?; if row.try_get::<String, _>("raw_head_event_id")? != visibility.event.event_id || row.try_get::<i64, _>("raw_head_event_seq")? != visibility.event.seq - || row.try_get::<i64, _>("raw_head_created_at")? - != i64::try_from(visibility.event.created_at).map_err(|_| { - RadrootsEventStoreError::CurrentVisibilityDrift { - reason: format!( - "addressable event `{}` timestamp is outside SQLite range", - visibility.event.event_id - ), - } - })? + || stored_created_at != visibility.event.created_at || row.try_get::<String, _>("admission_status")? != visibility.event.admission_status.as_str() || row.try_get::<Option<String>, _>("admission_code")? diff --git a/tools/xtask/src/contract/phase1_publication_artifact.rs b/tools/xtask/src/contract/phase1_publication_artifact.rs @@ -1,6 +1,9 @@ use super::artifact_bundle::{ GeneratedArtifact, read_regular_file, with_artifact_bundle_transaction, }; +use super::raw_source_rebuild::{ + is_semantic_event_store_production_source, validate_event_store_production_source_authority, +}; use radroots_event_codec::wire::publication::{ RADROOTS_PHASE1_PUBLICATION_ARTIFACT_MAX_BYTES, RADROOTS_PHASE1_PUBLICATION_ARTIFACT_SCHEMA_VERSION, @@ -1378,6 +1381,7 @@ fn validate_result_vector(workspace_root: &Path) -> Result<ValidatedResultVector } fn validate_immutable_raw_predecessor_under_lock(workspace_root: &Path) -> Result<(), String> { + validate_event_store_production_source_authority(workspace_root)?; for spec in RAW_IMMUTABLE_ARTIFACTS { let bytes = read_regular_file(workspace_root, spec.relative)?; if bytes.len() != spec.byte_length || sha256_hex(&bytes) != spec.sha256 { @@ -1434,7 +1438,7 @@ fn validate_immutable_raw_predecessor_under_lock(workspace_root: &Path) -> Resul if !seen.insert(path) { return Err(format!("{RAW_MANIFEST_RELATIVE} duplicates source {path}")); } - if !superseded.contains(path) { + if !superseded.contains(path) && !is_semantic_event_store_production_source(path) { validate_value_descriptor(workspace_root, source, "raw predecessor source")?; } } diff --git a/tools/xtask/src/contract/raw_source_rebuild.rs b/tools/xtask/src/contract/raw_source_rebuild.rs @@ -1217,7 +1217,7 @@ fn validate_event_store_production_source_inventory( let source = std::str::from_utf8(&source_bytes).map_err(|error| { format!("{EVENT_STORE_PRODUCTION_SOURCES_RELATIVE} must be UTF-8 TOML: {error}") })?; - let inventory: EventStoreProductionSourceInventory = toml::from_str(&source) + let inventory: EventStoreProductionSourceInventory = toml::from_str(source) .map_err(|error| format!("parse {EVENT_STORE_PRODUCTION_SOURCES_RELATIVE}: {error}"))?; if inventory.schema_version != 1 || inventory.hash_algorithm != PRODUCTION_AST_HASH_ALGORITHM { return Err(format!( @@ -1266,7 +1266,7 @@ fn validate_event_store_production_source_inventory( Ok(actual_paths) } -fn is_semantic_event_store_production_source(path: &str) -> bool { +pub(super) fn is_semantic_event_store_production_source(path: &str) -> bool { path.starts_with("crates/event_store/src/") && path.ends_with(".rs") && path != "crates/event_store/src/generated.rs"