rhi

Coordinated trade for connected markets
git clone https://radroots.dev/git/rhi.git
Log | Files | Refs | README | LICENSE

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 }