v0.3.4-pre.007
This commit is contained in:
230
crates/ksp-store-postgres-lib/src/raw_account/cursor.rs
Normal file
230
crates/ksp-store-postgres-lib/src/raw_account/cursor.rs
Normal file
@@ -0,0 +1,230 @@
|
||||
// file: crates/ksp-store-postgres-lib/src/raw_account/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-account-state-cursor/v1";
|
||||
const CURSOR_MAGIC: &[u8; 4] = b"KSPA";
|
||||
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 account cursor.
|
||||
pub(crate) struct RawAccountDecodedCursor {
|
||||
/// Last canonical account pubkey returned by the previous page.
|
||||
pub(crate) last_pubkey: [u8; 32],
|
||||
/// Last canonical account-state hash returned by the previous page.
|
||||
pub(crate) last_state_hash: [u8; 32],
|
||||
/// Last canonical account slot returned by the previous page.
|
||||
pub(crate) last_slot: u64,
|
||||
}
|
||||
|
||||
/// Decodes and validates one backend-private RAW account cursor against its query binding.
|
||||
pub(crate) fn decode_raw_account_cursor(
|
||||
query: &ksp_store_api::RawAccountStateQuery,
|
||||
cursor: &ksp_store_api::RawPageCursor,
|
||||
) -> std::result::Result<RawAccountDecodedCursor, crate::PostgresBackendError> {
|
||||
let bytes = cursor.as_bytes();
|
||||
if bytes.len() != CURSOR_BYTES {
|
||||
return std::result::Result::Err(query_invalid("raw_account_cursor_size"));
|
||||
}
|
||||
if bytes.get(0..4) != std::option::Option::Some(CURSOR_MAGIC.as_ref()) {
|
||||
return std::result::Result::Err(query_invalid("raw_account_cursor_magic"));
|
||||
}
|
||||
if bytes.get(4).copied() != std::option::Option::Some(CURSOR_VERSION) {
|
||||
return std::result::Result::Err(query_invalid("raw_account_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_account_cursor_slot")),
|
||||
},
|
||||
std::option::Option::None => return std::result::Result::Err(query_invalid("raw_account_cursor_slot")),
|
||||
};
|
||||
let last_slot = u64::from_be_bytes(slot_bytes);
|
||||
let last_pubkey = match bytes.get(13..45) {
|
||||
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_account_cursor_pubkey")),
|
||||
},
|
||||
std::option::Option::None => return std::result::Result::Err(query_invalid("raw_account_cursor_pubkey")),
|
||||
};
|
||||
let last_state_hash = match bytes.get(45..CURSOR_DIGEST_OFFSET) {
|
||||
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_account_cursor_state_hash")),
|
||||
},
|
||||
std::option::Option::None => return std::result::Result::Err(query_invalid("raw_account_cursor_state_hash")),
|
||||
};
|
||||
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 pubkey_result = validate_pubkey_filter(query.pubkey(), &last_pubkey);
|
||||
if let std::result::Result::Err(error) = pubkey_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let expected_digest = match binding_digest(query, last_slot, &last_pubkey, &last_state_hash) {
|
||||
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_account_cursor_digest")),
|
||||
},
|
||||
std::option::Option::None => return std::result::Result::Err(query_invalid("raw_account_cursor_digest")),
|
||||
};
|
||||
if stored_digest != expected_digest {
|
||||
return std::result::Result::Err(query_invalid("raw_account_cursor_binding"));
|
||||
}
|
||||
return std::result::Result::Ok(RawAccountDecodedCursor { last_pubkey, last_state_hash, last_slot });
|
||||
}
|
||||
|
||||
/// Encodes one backend-private RAW account cursor bound to the supplied query context.
|
||||
pub(crate) fn encode_raw_account_cursor(
|
||||
query: &ksp_store_api::RawAccountStateQuery,
|
||||
last_slot: u64,
|
||||
last_pubkey: &ksp_store_api::Pubkey,
|
||||
last_state_hash: &ksp_store_api::RawContentHash,
|
||||
) -> 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 pubkey_bytes = match <[u8; 32]>::try_from(last_pubkey.as_ref()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(query_invalid("raw_account_cursor_pubkey")),
|
||||
};
|
||||
let pubkey_result = validate_pubkey_filter(query.pubkey(), &pubkey_bytes);
|
||||
if let std::result::Result::Err(error) = pubkey_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let digest = match binding_digest(query, last_slot, &pubkey_bytes, last_state_hash.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(&pubkey_bytes);
|
||||
bytes.extend_from_slice(last_state_hash.as_bytes());
|
||||
bytes.extend_from_slice(&digest);
|
||||
if bytes.len() != CURSOR_BYTES {
|
||||
return std::result::Result::Err(query_invalid("raw_account_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_account_cursor_encode")),
|
||||
};
|
||||
}
|
||||
|
||||
/// Converts one caller page size into the exact PostgreSQL LIMIT+1 representation.
|
||||
pub(crate) fn raw_account_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_account_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_account_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_account_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_account_page_limit"));
|
||||
},
|
||||
};
|
||||
return std::result::Result::Ok((requested_usize, sql_limit));
|
||||
}
|
||||
|
||||
fn binding_digest(
|
||||
query: &ksp_store_api::RawAccountStateQuery,
|
||||
last_slot: u64,
|
||||
last_pubkey: &[u8; 32],
|
||||
last_state_hash: &[u8; 32],
|
||||
) -> 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_account_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_account_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());
|
||||
hash_optional_pubkey(&mut hasher, query.pubkey());
|
||||
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_pubkey);
|
||||
hasher.update(last_state_hash);
|
||||
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_pubkey(hasher: &mut sha2::Sha256, value: std::option::Option<&ksp_store_api::Pubkey>) {
|
||||
match value {
|
||||
std::option::Option::Some(pubkey) => {
|
||||
hasher.update([1_u8]);
|
||||
hasher.update(pubkey.as_ref());
|
||||
},
|
||||
std::option::Option::None => hasher.update([0_u8]),
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
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 query_invalid(phase: &'static str) -> crate::PostgresBackendError {
|
||||
return crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::QueryInvalid, phase);
|
||||
}
|
||||
|
||||
fn validate_pubkey_filter(filter: std::option::Option<&ksp_store_api::Pubkey>, last_pubkey: &[u8; 32]) -> std::result::Result<(), crate::PostgresBackendError> {
|
||||
if let std::option::Option::Some(pubkey) = filter
|
||||
&& pubkey.as_ref() != last_pubkey.as_slice()
|
||||
{
|
||||
return std::result::Result::Err(query_invalid("raw_account_cursor_pubkey_filter"));
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
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_account_cursor_slot_range"));
|
||||
}
|
||||
if let std::option::Option::Some(end) = range.end_inclusive()
|
||||
&& slot > end
|
||||
{
|
||||
return std::result::Result::Err(query_invalid("raw_account_cursor_slot_range"));
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
Reference in New Issue
Block a user