185 lines
8.6 KiB
Rust
185 lines
8.6 KiB
Rust
// 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<RawTransactionDecodedCursor, crate::PostgresBackendError> {
|
|
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<ksp_store_api::RawPageCursor, crate::PostgresBackendError> {
|
|
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<u64>) {
|
|
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);
|
|
}
|