state_trade.rs (22406B)
1 //! Atomic immutable persistence for admitted trade-event evidence. 2 3 use core::fmt; 4 use std::{error::Error, sync::Arc}; 5 6 use radroots_service_sqlite::{ 7 ServiceSqliteTransaction, ServiceSqliteTransactionError, ServiceSqliteTransactionErrorKind, 8 }; 9 use serde_json::Value; 10 use sqlx::Row; 11 12 use crate::{ 13 RhiAdmittedTradeMutationEvent, RhiConfigDocumentV1, RhiEvidencePolicyDigest, RhiStateHostMode, 14 RhiStateRepositories, RhiTradeMutationObservedAtUnixSeconds, state_metadata, 15 }; 16 17 /// Exact version of the immutable trade-evidence persistence contract. 18 pub const RHI_TRADE_EVIDENCE_PERSISTENCE_CONTRACT_VERSION: u32 = 1; 19 20 const SOURCE_SELECTOR: &str = "trade_mutation_lineage_v1"; 21 const MAX_SOURCE_ID_BYTES: usize = 64; 22 const MAX_CONTRACT_ID_BYTES: usize = 128; 23 const MAX_MUTATION_CONTENT_BYTES: usize = 131_072; 24 const MAX_CANONICAL_EVENT_BYTES: usize = 524_288; 25 26 const INSERT_MUTATION_SQL: &str = r#"INSERT INTO trade_mutations ( 27 mutation_id, trade_id, contract_id, schema_version, event_kind, 28 author_pubkey, canonical_content 29 ) VALUES (?, ?, ?, ?, ?, ?, ?) 30 ON CONFLICT (mutation_id) DO NOTHING"#; 31 const READ_MUTATION_SQL: &str = r#"SELECT 32 length(trade_id) AS trade_id_bytes, 33 substr(trade_id, 1, 17) AS trade_id, 34 length(CAST(contract_id AS BLOB)) AS contract_id_bytes, 35 substr(contract_id, 1, 129) AS contract_id, 36 schema_version, 37 event_kind, 38 length(author_pubkey) AS author_pubkey_bytes, 39 substr(author_pubkey, 1, 33) AS author_pubkey, 40 length(canonical_content) AS canonical_content_bytes, 41 substr(canonical_content, 1, 131073) AS canonical_content 42 FROM trade_mutations 43 WHERE mutation_id = ? 44 LIMIT 1"#; 45 const INSERT_EVENT_SQL: &str = r#"INSERT INTO nostr_events ( 46 event_id, event_signature, mutation_id, author_pubkey, event_kind, 47 authored_at_unix_s, canonical_event_json 48 ) VALUES (?, ?, ?, ?, ?, ?, ?) 49 ON CONFLICT (event_id, event_signature) DO NOTHING"#; 50 const READ_EVENT_SQL: &str = r#"SELECT 51 length(mutation_id) AS mutation_id_bytes, 52 substr(mutation_id, 1, 33) AS mutation_id, 53 length(author_pubkey) AS author_pubkey_bytes, 54 substr(author_pubkey, 1, 33) AS author_pubkey, 55 event_kind, 56 authored_at_unix_s, 57 length(canonical_event_json) AS canonical_event_json_bytes, 58 substr(canonical_event_json, 1, 524289) AS canonical_event_json 59 FROM nostr_events 60 WHERE event_id = ? AND event_signature = ? 61 LIMIT 1"#; 62 const INSERT_OBSERVATION_SQL: &str = r#"INSERT INTO relay_observations ( 63 source_id, selector_id, evidence_policy_sha256, event_id, 64 event_signature, observed_at_unix_s 65 ) VALUES (?, ?, ?, ?, ?, ?) 66 ON CONFLICT ( 67 source_id, selector_id, evidence_policy_sha256, event_id, 68 event_signature, observed_at_unix_s 69 ) DO NOTHING"#; 70 71 /// One accepted configured source observation bound to an admitted signed event. 72 /// 73 /// Construction is sealed to a validated RHI configuration and an admitted 74 /// event, so arbitrary source labels, selectors, policy digests, identifiers, 75 /// signatures, and observation times cannot be supplied independently. 76 pub struct RhiTradeSourceObservation { 77 source_id: Box<str>, 78 policy: RhiEvidencePolicyDigest, 79 event_id: [u8; 32], 80 event_signature: [u8; 64], 81 observed_at: RhiTradeMutationObservedAtUnixSeconds, 82 } 83 84 impl RhiTradeSourceObservation { 85 /// Binds one admitted event to an exact configured `nostr_relay` source. 86 pub fn from_config( 87 configuration: &RhiConfigDocumentV1, 88 source_id: &str, 89 event: &RhiAdmittedTradeMutationEvent, 90 ) -> Result<Self, RhiTradeEvidencePersistenceError> { 91 let source = configured_source(configuration.normalized(), source_id) 92 .ok_or_else(|| failure(RhiTradeEvidencePersistenceErrorKind::InvalidObservation))?; 93 if source.pointer("/kind").and_then(Value::as_str) != Some("nostr_relay") 94 || source.pointer("/selector").and_then(Value::as_str) != Some(SOURCE_SELECTOR) 95 { 96 return Err(failure( 97 RhiTradeEvidencePersistenceErrorKind::InvalidObservation, 98 )); 99 } 100 let policy = state_metadata::evidence_policy_digest(configuration.normalized()) 101 .map_err(|_| failure(RhiTradeEvidencePersistenceErrorKind::InvalidObservation))?; 102 Ok(Self { 103 source_id: source_id.into(), 104 policy, 105 event_id: *event.event_id().as_bytes(), 106 event_signature: event.event_signature_bytes(), 107 observed_at: event.observed_at_unix_seconds(), 108 }) 109 } 110 111 pub(crate) fn from_parts( 112 source_id: Box<str>, 113 policy: RhiEvidencePolicyDigest, 114 event: &RhiAdmittedTradeMutationEvent, 115 ) -> Self { 116 Self { 117 source_id, 118 policy, 119 event_id: *event.event_id().as_bytes(), 120 event_signature: event.event_signature_bytes(), 121 observed_at: event.observed_at_unix_seconds(), 122 } 123 } 124 125 pub(crate) fn from_persistence_parts( 126 source_id: Box<str>, 127 policy: RhiEvidencePolicyDigest, 128 record: &PersistenceRecord, 129 observed_at: RhiTradeMutationObservedAtUnixSeconds, 130 ) -> Self { 131 Self { 132 source_id, 133 policy, 134 event_id: record.event_id, 135 event_signature: record.event_signature, 136 observed_at, 137 } 138 } 139 } 140 141 impl fmt::Debug for RhiTradeSourceObservation { 142 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 143 formatter 144 .debug_struct("RhiTradeSourceObservation") 145 .field("selector", &SOURCE_SELECTOR) 146 .field("observed_at_unix_seconds", &self.observed_at.get()) 147 .field("source", &"[redacted]") 148 .field("event", &"[redacted]") 149 .finish() 150 } 151 } 152 153 /// Stable source-free classification for immutable evidence persistence. 154 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 155 pub enum RhiTradeEvidencePersistenceErrorKind { 156 InvalidMode, 157 InvalidObservation, 158 Encoding, 159 MutationConflict, 160 SignedEventConflict, 161 Storage, 162 CommitOutcomeUnknown, 163 } 164 165 impl RhiTradeEvidencePersistenceErrorKind { 166 /// Returns the stable machine-readable failure code. 167 #[must_use] 168 pub const fn code(self) -> &'static str { 169 match self { 170 Self::InvalidMode => "trade_evidence_mode_invalid", 171 Self::InvalidObservation => "trade_evidence_observation_invalid", 172 Self::Encoding => "trade_evidence_encoding_failed", 173 Self::MutationConflict => "trade_mutation_conflict", 174 Self::SignedEventConflict => "trade_signed_event_conflict", 175 Self::Storage => "trade_evidence_storage_failed", 176 Self::CommitOutcomeUnknown => "trade_evidence_commit_outcome_unknown", 177 } 178 } 179 } 180 181 /// Redacted source-free immutable evidence persistence failure. 182 #[derive(Clone, Copy, PartialEq, Eq)] 183 pub struct RhiTradeEvidencePersistenceError { 184 kind: RhiTradeEvidencePersistenceErrorKind, 185 } 186 187 impl RhiTradeEvidencePersistenceError { 188 /// Returns the stable failure classification. 189 #[must_use] 190 pub const fn kind(self) -> RhiTradeEvidencePersistenceErrorKind { 191 self.kind 192 } 193 194 /// Returns the stable machine-readable failure code. 195 #[must_use] 196 pub const fn code(self) -> &'static str { 197 self.kind.code() 198 } 199 } 200 201 impl fmt::Display for RhiTradeEvidencePersistenceError { 202 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 203 formatter.write_str(match self.kind { 204 RhiTradeEvidencePersistenceErrorKind::InvalidMode => { 205 "RHI trade evidence requires writable state" 206 } 207 RhiTradeEvidencePersistenceErrorKind::InvalidObservation => { 208 "RHI trade source observation is invalid" 209 } 210 RhiTradeEvidencePersistenceErrorKind::Encoding => { 211 "RHI signed trade event encoding failed" 212 } 213 RhiTradeEvidencePersistenceErrorKind::MutationConflict => { 214 "RHI canonical trade mutation conflicts with durable evidence" 215 } 216 RhiTradeEvidencePersistenceErrorKind::SignedEventConflict => { 217 "RHI signed trade event conflicts with durable evidence" 218 } 219 RhiTradeEvidencePersistenceErrorKind::Storage => { 220 "RHI trade evidence transaction failed" 221 } 222 RhiTradeEvidencePersistenceErrorKind::CommitOutcomeUnknown => { 223 "RHI trade evidence commit outcome is unknown" 224 } 225 }) 226 } 227 } 228 229 impl fmt::Debug for RhiTradeEvidencePersistenceError { 230 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 231 formatter 232 .debug_struct("RhiTradeEvidencePersistenceError") 233 .field("kind", &self.kind) 234 .finish() 235 } 236 } 237 238 impl Error for RhiTradeEvidencePersistenceError {} 239 240 /// Exact immutable facts newly inserted by one committed persistence call. 241 #[derive(Clone, Copy, PartialEq, Eq)] 242 pub struct RhiTradeEvidencePersistenceOutcome { 243 mutation_inserted: bool, 244 signed_event_inserted: bool, 245 observation_inserted: bool, 246 } 247 248 impl RhiTradeEvidencePersistenceOutcome { 249 /// Returns whether the canonical mutation fact was newly inserted. 250 #[must_use] 251 pub const fn mutation_inserted(self) -> bool { 252 self.mutation_inserted 253 } 254 255 /// Returns whether the exact signed-event fact was newly inserted. 256 #[must_use] 257 pub const fn signed_event_inserted(self) -> bool { 258 self.signed_event_inserted 259 } 260 261 /// Returns whether the configured-source observation was newly inserted. 262 #[must_use] 263 pub const fn observation_inserted(self) -> bool { 264 self.observation_inserted 265 } 266 } 267 268 impl fmt::Debug for RhiTradeEvidencePersistenceOutcome { 269 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 270 formatter 271 .debug_struct("RhiTradeEvidencePersistenceOutcome") 272 .field("mutation_inserted", &self.mutation_inserted) 273 .field("signed_event_inserted", &self.signed_event_inserted) 274 .field("observation_inserted", &self.observation_inserted) 275 .finish() 276 } 277 } 278 279 impl RhiStateRepositories<'_> { 280 /// Atomically persists one admitted mutation, signed event, and observation. 281 pub async fn persist_trade_evidence( 282 &self, 283 event: RhiAdmittedTradeMutationEvent, 284 observation: RhiTradeSourceObservation, 285 ) -> Result<RhiTradeEvidencePersistenceOutcome, RhiTradeEvidencePersistenceError> { 286 let host = self.host(); 287 if host.mode() != RhiStateHostMode::ReadWriteExisting { 288 return Err(failure(RhiTradeEvidencePersistenceErrorKind::InvalidMode)); 289 } 290 let record = PersistenceRecord::from_admitted(event)?; 291 if observation.policy != host.metadata().evidence_policy_digest() 292 || observation.event_id != record.event_id 293 || observation.event_signature != record.event_signature 294 { 295 return Err(failure( 296 RhiTradeEvidencePersistenceErrorKind::InvalidObservation, 297 )); 298 } 299 host.sqlite_host() 300 .transaction(move |transaction| { 301 Box::pin(async move { persist(transaction, &record, &observation).await }) 302 }) 303 .await 304 .map_err(map_transaction_error) 305 } 306 } 307 308 #[derive(Clone)] 309 pub(crate) struct PersistenceRecord { 310 pub(crate) mutation_id: [u8; 32], 311 pub(crate) trade_id: [u8; 16], 312 pub(crate) contract_id: &'static str, 313 pub(crate) schema_version: u16, 314 pub(crate) event_id: [u8; 32], 315 pub(crate) event_signature: [u8; 64], 316 pub(crate) author_pubkey: [u8; 32], 317 pub(crate) event_kind: u32, 318 pub(crate) authored_at_unix_s: u64, 319 pub(crate) canonical_content: Arc<[u8]>, 320 pub(crate) canonical_event_json: Box<[u8]>, 321 } 322 323 impl PersistenceRecord { 324 pub(crate) fn from_admitted( 325 admitted: RhiAdmittedTradeMutationEvent, 326 ) -> Result<Self, RhiTradeEvidencePersistenceError> { 327 let (_original, event, mutation, mutation_id, _) = admitted.into_parts(); 328 let canonical_event_json = serde_json::to_vec(&event.to_nip01_wire()) 329 .map_err(|_| failure(RhiTradeEvidencePersistenceErrorKind::Encoding))?; 330 if canonical_event_json.is_empty() || canonical_event_json.len() > MAX_CANONICAL_EVENT_BYTES 331 { 332 return Err(failure(RhiTradeEvidencePersistenceErrorKind::Encoding)); 333 } 334 let canonical_content = event.content().as_bytes().to_vec(); 335 if canonical_content.is_empty() || canonical_content.len() > MAX_MUTATION_CONTENT_BYTES { 336 return Err(failure(RhiTradeEvidencePersistenceErrorKind::Encoding)); 337 } 338 Ok(Self { 339 mutation_id: *mutation_id.as_bytes(), 340 trade_id: *mutation.trade_id.as_bytes(), 341 contract_id: mutation.mutation_kind().contract_id(), 342 schema_version: mutation.schema_version, 343 event_id: *event.id().as_bytes(), 344 event_signature: *event.sig().as_bytes(), 345 author_pubkey: *event.author().as_bytes(), 346 event_kind: event.kind_u32(), 347 authored_at_unix_s: event.created_at_u64(), 348 canonical_content: canonical_content.into(), 349 canonical_event_json: canonical_event_json.into_boxed_slice(), 350 }) 351 } 352 } 353 354 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 355 pub(crate) enum PersistenceOperationError { 356 MutationConflict, 357 SignedEventConflict, 358 Storage, 359 } 360 361 pub(crate) async fn persist( 362 transaction: &mut ServiceSqliteTransaction<'_>, 363 record: &PersistenceRecord, 364 observation: &RhiTradeSourceObservation, 365 ) -> Result<RhiTradeEvidencePersistenceOutcome, PersistenceOperationError> { 366 let mutation_inserted = insert_mutation(transaction, record).await?; 367 let signed_event_inserted = insert_event(transaction, record).await?; 368 let observation_inserted = insert_observation(transaction, observation).await?; 369 Ok(RhiTradeEvidencePersistenceOutcome { 370 mutation_inserted, 371 signed_event_inserted, 372 observation_inserted, 373 }) 374 } 375 376 async fn insert_mutation( 377 transaction: &mut ServiceSqliteTransaction<'_>, 378 record: &PersistenceRecord, 379 ) -> Result<bool, PersistenceOperationError> { 380 let result = sqlx::query(INSERT_MUTATION_SQL) 381 .bind(record.mutation_id.as_slice()) 382 .bind(record.trade_id.as_slice()) 383 .bind(record.contract_id) 384 .bind(i64::from(record.schema_version)) 385 .bind(i64::from(record.event_kind)) 386 .bind(record.author_pubkey.as_slice()) 387 .bind(record.canonical_content.as_ref()) 388 .execute(&mut *transaction) 389 .await 390 .map_err(|_| PersistenceOperationError::Storage)?; 391 match result.rows_affected() { 392 1 => Ok(true), 393 0 if mutation_matches(transaction, record).await? => Ok(false), 394 0 => Err(PersistenceOperationError::MutationConflict), 395 _ => Err(PersistenceOperationError::Storage), 396 } 397 } 398 399 async fn mutation_matches( 400 transaction: &mut ServiceSqliteTransaction<'_>, 401 expected: &PersistenceRecord, 402 ) -> Result<bool, PersistenceOperationError> { 403 let row = sqlx::query(READ_MUTATION_SQL) 404 .bind(expected.mutation_id.as_slice()) 405 .fetch_optional(&mut *transaction) 406 .await 407 .map_err(|_| PersistenceOperationError::Storage)? 408 .ok_or(PersistenceOperationError::Storage)?; 409 Ok( 410 exact_blob(&row, "trade_id", "trade_id_bytes")? == expected.trade_id 411 && bounded_text( 412 &row, 413 "contract_id", 414 "contract_id_bytes", 415 MAX_CONTRACT_ID_BYTES, 416 )? == expected.contract_id 417 && row.try_get::<i64, _>("schema_version").ok() 418 == Some(i64::from(expected.schema_version)) 419 && row.try_get::<i64, _>("event_kind").ok() == Some(i64::from(expected.event_kind)) 420 && exact_blob(&row, "author_pubkey", "author_pubkey_bytes")? == expected.author_pubkey 421 && bounded_blob( 422 &row, 423 "canonical_content", 424 "canonical_content_bytes", 425 MAX_MUTATION_CONTENT_BYTES, 426 )? == expected.canonical_content.as_ref(), 427 ) 428 } 429 430 async fn insert_event( 431 transaction: &mut ServiceSqliteTransaction<'_>, 432 record: &PersistenceRecord, 433 ) -> Result<bool, PersistenceOperationError> { 434 let result = sqlx::query(INSERT_EVENT_SQL) 435 .bind(record.event_id.as_slice()) 436 .bind(record.event_signature.as_slice()) 437 .bind(record.mutation_id.as_slice()) 438 .bind(record.author_pubkey.as_slice()) 439 .bind(i64::from(record.event_kind)) 440 .bind( 441 i64::try_from(record.authored_at_unix_s) 442 .map_err(|_| PersistenceOperationError::Storage)?, 443 ) 444 .bind(record.canonical_event_json.as_ref()) 445 .execute(&mut *transaction) 446 .await 447 .map_err(|_| PersistenceOperationError::Storage)?; 448 match result.rows_affected() { 449 1 => Ok(true), 450 0 if event_matches(transaction, record).await? => Ok(false), 451 0 => Err(PersistenceOperationError::SignedEventConflict), 452 _ => Err(PersistenceOperationError::Storage), 453 } 454 } 455 456 async fn event_matches( 457 transaction: &mut ServiceSqliteTransaction<'_>, 458 expected: &PersistenceRecord, 459 ) -> Result<bool, PersistenceOperationError> { 460 let row = sqlx::query(READ_EVENT_SQL) 461 .bind(expected.event_id.as_slice()) 462 .bind(expected.event_signature.as_slice()) 463 .fetch_optional(&mut *transaction) 464 .await 465 .map_err(|_| PersistenceOperationError::Storage)? 466 .ok_or(PersistenceOperationError::Storage)?; 467 Ok( 468 exact_blob(&row, "mutation_id", "mutation_id_bytes")? == expected.mutation_id 469 && exact_blob(&row, "author_pubkey", "author_pubkey_bytes")? == expected.author_pubkey 470 && row.try_get::<i64, _>("event_kind").ok() == Some(i64::from(expected.event_kind)) 471 && row.try_get::<i64, _>("authored_at_unix_s").ok() 472 == i64::try_from(expected.authored_at_unix_s).ok() 473 && bounded_blob( 474 &row, 475 "canonical_event_json", 476 "canonical_event_json_bytes", 477 MAX_CANONICAL_EVENT_BYTES, 478 )? == expected.canonical_event_json.as_ref(), 479 ) 480 } 481 482 async fn insert_observation( 483 transaction: &mut ServiceSqliteTransaction<'_>, 484 observation: &RhiTradeSourceObservation, 485 ) -> Result<bool, PersistenceOperationError> { 486 let result = sqlx::query(INSERT_OBSERVATION_SQL) 487 .bind(observation.source_id.as_ref()) 488 .bind(SOURCE_SELECTOR) 489 .bind(observation.policy.as_bytes().as_slice()) 490 .bind(observation.event_id.as_slice()) 491 .bind(observation.event_signature.as_slice()) 492 .bind( 493 i64::try_from(observation.observed_at.get()) 494 .map_err(|_| PersistenceOperationError::Storage)?, 495 ) 496 .execute(&mut *transaction) 497 .await 498 .map_err(|_| PersistenceOperationError::Storage)?; 499 match result.rows_affected() { 500 0 => Ok(false), 501 1 => Ok(true), 502 _ => Err(PersistenceOperationError::Storage), 503 } 504 } 505 506 fn configured_source<'a>(configuration: &'a Value, source_id: &str) -> Option<&'a Value> { 507 if source_id.is_empty() 508 || source_id.len() > MAX_SOURCE_ID_BYTES 509 || !source_id.bytes().enumerate().all(|(index, byte)| { 510 if index == 0 { 511 byte.is_ascii_lowercase() 512 } else { 513 byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'_' | b'-') 514 } 515 }) 516 { 517 return None; 518 } 519 configuration 520 .pointer("/evidence/sources")? 521 .as_array()? 522 .iter() 523 .find(|source| source.pointer("/source_id").and_then(Value::as_str) == Some(source_id)) 524 } 525 526 fn exact_blob<const N: usize>( 527 row: &sqlx::sqlite::SqliteRow, 528 field: &str, 529 length_field: &str, 530 ) -> Result<[u8; N], PersistenceOperationError> { 531 bounded_blob(row, field, length_field, N)? 532 .try_into() 533 .map_err(|_| PersistenceOperationError::Storage) 534 } 535 536 fn bounded_text( 537 row: &sqlx::sqlite::SqliteRow, 538 field: &str, 539 length_field: &str, 540 maximum: usize, 541 ) -> Result<String, PersistenceOperationError> { 542 let length = bounded_length(row, length_field, maximum)?; 543 let value = row 544 .try_get::<String, _>(field) 545 .map_err(|_| PersistenceOperationError::Storage)?; 546 (value.len() == length) 547 .then_some(value) 548 .ok_or(PersistenceOperationError::Storage) 549 } 550 551 fn bounded_blob( 552 row: &sqlx::sqlite::SqliteRow, 553 field: &str, 554 length_field: &str, 555 maximum: usize, 556 ) -> Result<Vec<u8>, PersistenceOperationError> { 557 let length = bounded_length(row, length_field, maximum)?; 558 let value = row 559 .try_get::<Vec<u8>, _>(field) 560 .map_err(|_| PersistenceOperationError::Storage)?; 561 (value.len() == length) 562 .then_some(value) 563 .ok_or(PersistenceOperationError::Storage) 564 } 565 566 fn bounded_length( 567 row: &sqlx::sqlite::SqliteRow, 568 field: &str, 569 maximum: usize, 570 ) -> Result<usize, PersistenceOperationError> { 571 row.try_get::<i64, _>(field) 572 .ok() 573 .and_then(|value| usize::try_from(value).ok()) 574 .filter(|value| (1..=maximum).contains(value)) 575 .ok_or(PersistenceOperationError::Storage) 576 } 577 578 fn map_transaction_error( 579 error: ServiceSqliteTransactionError<PersistenceOperationError>, 580 ) -> RhiTradeEvidencePersistenceError { 581 if error.kind() == ServiceSqliteTransactionErrorKind::CommitOutcomeUnknown { 582 return failure(RhiTradeEvidencePersistenceErrorKind::CommitOutcomeUnknown); 583 } 584 failure(match error.operation_error().copied() { 585 Some(PersistenceOperationError::MutationConflict) => { 586 RhiTradeEvidencePersistenceErrorKind::MutationConflict 587 } 588 Some(PersistenceOperationError::SignedEventConflict) => { 589 RhiTradeEvidencePersistenceErrorKind::SignedEventConflict 590 } 591 Some(PersistenceOperationError::Storage) | None => { 592 RhiTradeEvidencePersistenceErrorKind::Storage 593 } 594 }) 595 } 596 597 const fn failure(kind: RhiTradeEvidencePersistenceErrorKind) -> RhiTradeEvidencePersistenceError { 598 RhiTradeEvidencePersistenceError { kind } 599 }