// file: crates/ksp-store-postgres-lib/src/raw_transaction/cursor.rs // version: 1 use sha2::Digest; // rust-rules: trait-import const CURSOR_BYTES: usize = 109; const CURSOR_DIGEST_OFFSET: usize = 77; const CURSOR_DOMAIN: &[u8] = b"KSP/raw-transaction-cursor/v1"; const CURSOR_MAGIC: &[u8; 4] = b"KSPT"; const CURSOR_VERSION: u8 = 1; const MAX_POSTGRES_PAGE_LIMIT: u64 = 9_223_372_036_854_775_806; #[derive(Clone, Copy, Debug, Eq, PartialEq)] /// Decoded backend-private continuation key extracted from one validated RAW cursor. pub(crate) struct RawTransactionDecodedCursor { /// Last canonical transaction signature returned by the previous page. pub(crate) last_signature: [u8; 64], /// Last canonical transaction slot returned by the previous page. pub(crate) last_slot: u64, } /// Decodes and validates one backend-private RAW transaction cursor against its query binding. pub(crate) fn decode_raw_transaction_cursor( query: &ksp_store_api::RawTransactionQuery, cursor: &ksp_store_api::RawPageCursor, ) -> std::result::Result { let bytes = cursor.as_bytes(); if bytes.len() != CURSOR_BYTES { return std::result::Result::Err(query_invalid("raw_cursor_size")); } if bytes.get(0..4) != std::option::Option::Some(CURSOR_MAGIC.as_ref()) { return std::result::Result::Err(query_invalid("raw_cursor_magic")); } if bytes.get(4).copied() != std::option::Option::Some(CURSOR_VERSION) { return std::result::Result::Err(query_invalid("raw_cursor_version")); } let slot_bytes = match bytes.get(5..13) { std::option::Option::Some(value) => match <[u8; 8]>::try_from(value) { std::result::Result::Ok(decoded) => decoded, std::result::Result::Err(_) => return std::result::Result::Err(query_invalid("raw_cursor_slot")), }, std::option::Option::None => return std::result::Result::Err(query_invalid("raw_cursor_slot")), }; let last_slot = u64::from_be_bytes(slot_bytes); let last_signature = match bytes.get(13..CURSOR_DIGEST_OFFSET) { std::option::Option::Some(value) => match <[u8; 64]>::try_from(value) { std::result::Result::Ok(decoded) => decoded, std::result::Result::Err(_) => return std::result::Result::Err(query_invalid("raw_cursor_signature")), }, std::option::Option::None => return std::result::Result::Err(query_invalid("raw_cursor_signature")), }; let range_result = validate_slot_in_range(query.slots(), last_slot); if let std::result::Result::Err(error) = range_result { return std::result::Result::Err(error); } let expected_digest = match binding_digest(query, last_slot, &last_signature) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let stored_digest = match bytes.get(CURSOR_DIGEST_OFFSET..CURSOR_BYTES) { std::option::Option::Some(value) => match <[u8; 32]>::try_from(value) { std::result::Result::Ok(decoded) => decoded, std::result::Result::Err(_) => return std::result::Result::Err(query_invalid("raw_cursor_digest")), }, std::option::Option::None => return std::result::Result::Err(query_invalid("raw_cursor_digest")), }; if stored_digest != expected_digest { return std::result::Result::Err(query_invalid("raw_cursor_binding")); } return std::result::Result::Ok(RawTransactionDecodedCursor { last_signature, last_slot }); } /// Encodes one backend-private RAW transaction cursor bound to the supplied query context. pub(crate) fn encode_raw_transaction_cursor( query: &ksp_store_api::RawTransactionQuery, last_slot: u64, last_signature: &ksp_store_api::RawTransactionSignature, ) -> std::result::Result { let range_result = validate_slot_in_range(query.slots(), last_slot); if let std::result::Result::Err(error) = range_result { return std::result::Result::Err(error); } let digest = match binding_digest(query, last_slot, last_signature.as_bytes()) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let mut bytes = std::vec::Vec::with_capacity(CURSOR_BYTES); bytes.extend_from_slice(CURSOR_MAGIC); bytes.push(CURSOR_VERSION); bytes.extend_from_slice(&last_slot.to_be_bytes()); bytes.extend_from_slice(last_signature.as_bytes()); bytes.extend_from_slice(&digest); if bytes.len() != CURSOR_BYTES { return std::result::Result::Err(query_invalid("raw_cursor_encode_size")); } return match ksp_store_api::RawPageCursor::try_new(bytes.into_boxed_slice()) { std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Err(_) => std::result::Result::Err(query_invalid("raw_cursor_encode")), }; } /// Converts one caller page size into the exact PostgreSQL LIMIT+1 representation. pub(crate) fn raw_transaction_physical_page_limit(requested: u64) -> std::result::Result<(usize, i64), crate::PostgresBackendError> { if requested > MAX_POSTGRES_PAGE_LIMIT { return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::PageLimitUnsupported, "raw_page_limit")); } let requested_usize = match usize::try_from(requested) { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => { return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::PageLimitUnsupported, "raw_page_limit")); }, }; let requested_plus_one = match requested.checked_add(1) { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::PageLimitUnsupported, "raw_page_limit")); }, }; let sql_limit = match i64::try_from(requested_plus_one) { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => { return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::PageLimitUnsupported, "raw_page_limit")); }, }; return std::result::Result::Ok((requested_usize, sql_limit)); } fn binding_digest( query: &ksp_store_api::RawTransactionQuery, last_slot: u64, last_signature: &[u8; 64], ) -> std::result::Result<[u8; 32], crate::PostgresBackendError> { let direction = match query.direction() { ksp_store_api::RawSortDirection::Ascending => 0_u8, ksp_store_api::RawSortDirection::Descending => 1_u8, _ => return std::result::Result::Err(query_invalid("raw_cursor_direction")), }; let network_len = match u64::try_from(query.network().as_str().len()) { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return std::result::Result::Err(query_invalid("raw_cursor_network")), }; let mut hasher = sha2::Sha256::new(); hasher.update(CURSOR_DOMAIN); hasher.update(network_len.to_be_bytes()); hasher.update(query.network().as_str().as_bytes()); hasher.update([direction]); hash_optional_slot(&mut hasher, query.slots().start_inclusive()); hash_optional_slot(&mut hasher, query.slots().end_inclusive()); hasher.update(last_slot.to_be_bytes()); hasher.update(last_signature); let finalized = hasher.finalize(); let mut digest = [0_u8; 32]; digest.copy_from_slice(finalized.as_ref()); return std::result::Result::Ok(digest); } fn hash_optional_slot(hasher: &mut sha2::Sha256, value: std::option::Option) { match value { std::option::Option::Some(slot) => { hasher.update([1_u8]); hasher.update(slot.to_be_bytes()); }, std::option::Option::None => hasher.update([0_u8]), } return; } fn validate_slot_in_range(range: ksp_store_api::RawSlotRange, slot: u64) -> std::result::Result<(), crate::PostgresBackendError> { if let std::option::Option::Some(start) = range.start_inclusive() && slot < start { return std::result::Result::Err(query_invalid("raw_cursor_slot_range")); } if let std::option::Option::Some(end) = range.end_inclusive() && slot > end { return std::result::Result::Err(query_invalid("raw_cursor_slot_range")); } return std::result::Result::Ok(()); } fn query_invalid(phase: &'static str) -> crate::PostgresBackendError { return crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::QueryInvalid, phase); }