// file: crates/ksp-job-backfill-lib/src/request.rs // version: 6 use sha2::Digest; // rust-rules: trait-import /// Maximum number of transaction candidates admitted by one bounded Backfill Job. pub const MAX_BACKFILL_CANDIDATES: usize = 10_000; /// Maximum number of concurrent transaction hydrations admitted by one Backfill request. pub const MAX_BACKFILL_HYDRATION_CONCURRENCY: usize = 64; /// Maximum number of `getSignaturesForAddress` pages admitted by one address Backfill request. pub const MAX_BACKFILL_PAGES: usize = 10_000; /// Maximum page size admitted for one `getSignaturesForAddress` request. pub const MAX_BACKFILL_PAGE_SIZE: usize = 1_000; /// Maximum Base58 text length possible for one canonical 64-byte Solana signature. pub const MAX_BACKFILL_SIGNATURE_TEXT_BYTES: usize = 88; /// Minimum Base58 text length possible for one canonical 64-byte Solana signature. pub const MIN_BACKFILL_SIGNATURE_TEXT_BYTES: usize = 64; /// Commitment levels intentionally admitted by the historical Backfill vertical. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum BackfillCommitment { /// Read history at Solana `confirmed` commitment. Confirmed, /// Read history at Solana `finalized` commitment. Finalized, } impl crate::BackfillCommitment { /// Returns the stable Backfill commitment code. #[must_use] pub const fn code(self) -> &'static str { return match self { Self::Confirmed => "confirmed", Self::Finalized => "finalized", }; } /// Maps the narrowed Backfill commitment to the Transport-owned Solana commitment. #[must_use] pub(crate) const fn transport(self) -> ksp_onchain_transport_lib::SolanaCommitment { return match self { Self::Confirmed => ksp_onchain_transport_lib::SolanaCommitment::Confirmed, Self::Finalized => ksp_onchain_transport_lib::SolanaCommitment::Finalized, }; } } /// Bounded Base58-shaped transaction signature text used by discovery and RAW conversion. /// /// Construction validates the encoded shape required by discovery. Exact conversion to the /// Store-owned 64-byte signature is available through [`Self::to_raw_transaction_signature`]. #[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)] pub struct BackfillSignature(std::string::String); impl crate::BackfillSignature { /// Creates one bounded Base58-shaped signature text. pub fn new(value: impl std::convert::Into) -> ksp_core_lib::Result { let value = value.into(); if !valid_signature_text(value.as_str()) { return std::result::Result::Err(signature_error()); } return std::result::Result::Ok(Self(value)); } /// Returns the validated encoded signature text. #[must_use] pub fn as_str(&self) -> &str { return self.0.as_str(); } /// Decodes this Base58 text to the exact Store-owned 64-byte Solana signature. pub fn to_raw_transaction_signature(&self) -> ksp_core_lib::Result { return crate::decode_backfill_signature(self); } } impl std::fmt::Debug for crate::BackfillSignature { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { return formatter.write_str("BackfillSignature(..)"); } } /// Stable category of one bounded Backfill discovery scope. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum BackfillScopeKind { /// Read the current newest address window. LatestAddress, /// Read address history older than one exclusive anchor. BeforeAddress, /// Read the bounded newer window closest to one exclusive anchor. AfterAddress, /// Hydrate an explicit bounded signature list without address discovery. ExplicitSignatures, } impl crate::BackfillScopeKind { /// Returns the stable scope code used by diagnostics and scope fingerprinting. #[must_use] pub const fn code(self) -> &'static str { return match self { Self::LatestAddress => "latest_address", Self::BeforeAddress => "before_address", Self::AfterAddress => "after_address", Self::ExplicitSignatures => "explicit_signatures", }; } } #[derive(Clone, Eq, PartialEq)] enum BackfillScopeValue { LatestAddress { address: ksp_core_lib::Pubkey }, BeforeAddress { address: ksp_core_lib::Pubkey, anchor: crate::BackfillSignature }, AfterAddress { address: ksp_core_lib::Pubkey, anchor: crate::BackfillSignature }, ExplicitSignatures { signatures: std::vec::Vec }, } /// Validated bounded discovery scope for one historical Backfill Job. #[derive(Clone, Eq, PartialEq)] pub struct BackfillScope { value: BackfillScopeValue, } impl crate::BackfillScope { /// Creates a scope starting at the newest known history for one address. #[must_use] pub const fn latest_address(address: ksp_core_lib::Pubkey) -> Self { return Self { value: BackfillScopeValue::LatestAddress { address } }; } /// Creates a scope reading history older than one exclusive address anchor. #[must_use] pub fn before_address(address: ksp_core_lib::Pubkey, anchor: crate::BackfillSignature) -> Self { return Self { value: BackfillScopeValue::BeforeAddress { address, anchor } }; } /// Creates a scope reading the bounded newer history closest to one exclusive address anchor. #[must_use] pub fn after_address(address: ksp_core_lib::Pubkey, anchor: crate::BackfillSignature) -> Self { return Self { value: BackfillScopeValue::AfterAddress { address, anchor } }; } /// Creates an explicit signature scope with stable first-occurrence deduplication. pub fn explicit_signatures(signatures: std::vec::Vec) -> ksp_core_lib::Result { if signatures.is_empty() || signatures.len() > crate::MAX_BACKFILL_CANDIDATES { return std::result::Result::Err(request_error("scope.signatures")); } let mut unique = std::vec::Vec::with_capacity(signatures.len()); let mut seen = std::collections::HashSet::with_capacity(signatures.len()); for signature in signatures { if seen.insert(signature.clone()) { unique.push(signature); } } if unique.is_empty() { return std::result::Result::Err(request_error("scope.signatures")); } return std::result::Result::Ok(Self { value: BackfillScopeValue::ExplicitSignatures { signatures: unique } }); } /// Returns the stable category of this scope. #[must_use] pub const fn kind(&self) -> crate::BackfillScopeKind { return match &self.value { BackfillScopeValue::LatestAddress { .. } => crate::BackfillScopeKind::LatestAddress, BackfillScopeValue::BeforeAddress { .. } => crate::BackfillScopeKind::BeforeAddress, BackfillScopeValue::AfterAddress { .. } => crate::BackfillScopeKind::AfterAddress, BackfillScopeValue::ExplicitSignatures { .. } => crate::BackfillScopeKind::ExplicitSignatures, }; } /// Returns the address used by address scopes. #[must_use] pub const fn address(&self) -> std::option::Option<&ksp_core_lib::Pubkey> { return match &self.value { BackfillScopeValue::LatestAddress { address } | BackfillScopeValue::BeforeAddress { address, .. } | BackfillScopeValue::AfterAddress { address, .. } => std::option::Option::Some(address), BackfillScopeValue::ExplicitSignatures { .. } => std::option::Option::None, }; } /// Returns the exclusive anchor used by before/after scopes. #[must_use] pub const fn anchor(&self) -> std::option::Option<&crate::BackfillSignature> { return match &self.value { BackfillScopeValue::BeforeAddress { anchor, .. } | BackfillScopeValue::AfterAddress { anchor, .. } => std::option::Option::Some(anchor), BackfillScopeValue::LatestAddress { .. } | BackfillScopeValue::ExplicitSignatures { .. } => std::option::Option::None, }; } /// Returns the stable deduplicated explicit signature list, when this is an explicit scope. #[must_use] pub fn signatures(&self) -> std::option::Option<&[crate::BackfillSignature]> { return match &self.value { BackfillScopeValue::ExplicitSignatures { signatures } => std::option::Option::Some(signatures.as_slice()), BackfillScopeValue::LatestAddress { .. } | BackfillScopeValue::BeforeAddress { .. } | BackfillScopeValue::AfterAddress { .. } => { std::option::Option::None }, }; } } impl std::fmt::Debug for crate::BackfillScope { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut debug = formatter.debug_struct("BackfillScope"); debug.field("kind", &self.kind()); match &self.value { BackfillScopeValue::LatestAddress { address } => { debug.field("address", address); }, BackfillScopeValue::BeforeAddress { address, .. } | BackfillScopeValue::AfterAddress { address, .. } => { debug.field("address", address).field("anchor", &""); }, BackfillScopeValue::ExplicitSignatures { signatures } => { debug.field("signature_count", &signatures.len()); }, } return debug.finish(); } } /// Opaque deterministic fingerprint of one semantic Backfill scope. #[derive(Clone, Copy, Eq, Hash, PartialEq)] pub struct BackfillScopeFingerprint([u8; 32]); impl crate::BackfillScopeFingerprint { const fn from_bytes(bytes: [u8; 32]) -> Self { return Self(bytes); } /// Returns the exact deterministic fingerprint bytes. #[must_use] pub const fn as_bytes(&self) -> &[u8; 32] { return &self.0; } } impl std::fmt::Debug for crate::BackfillScopeFingerprint { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { return formatter.write_str("BackfillScopeFingerprint(..)"); } } /// Fully explicit bounded request for one historical transaction Backfill Job. #[derive(Clone, Eq, PartialEq)] pub struct BackfillRequest { job_id: ksp_job_api::JobId, network: ksp_store_lib::RawNetworkId, role: ksp_onchain_transport_lib::HttpRoleName, commitment: crate::BackfillCommitment, scope: crate::BackfillScope, page_size: usize, max_pages: usize, max_candidates: usize, hydration_concurrency: usize, min_context_slot: std::option::Option, scope_fingerprint: crate::BackfillScopeFingerprint, checkpoint: std::option::Option, } impl crate::BackfillRequest { /// Creates and validates one fully explicit bounded Backfill request. #[allow(clippy::too_many_arguments)] pub fn new( job_id: ksp_job_api::JobId, network: ksp_store_lib::RawNetworkId, role: ksp_onchain_transport_lib::HttpRoleName, commitment: crate::BackfillCommitment, scope: crate::BackfillScope, page_size: usize, max_pages: usize, max_candidates: usize, hydration_concurrency: usize, min_context_slot: std::option::Option, ) -> ksp_core_lib::Result { if page_size == 0 || page_size > crate::MAX_BACKFILL_PAGE_SIZE { return std::result::Result::Err(request_error("page_size")); } if max_pages == 0 || max_pages > crate::MAX_BACKFILL_PAGES { return std::result::Result::Err(request_error("max_pages")); } if max_candidates == 0 || max_candidates > crate::MAX_BACKFILL_CANDIDATES { return std::result::Result::Err(request_error("max_candidates")); } if hydration_concurrency == 0 || hydration_concurrency > crate::MAX_BACKFILL_HYDRATION_CONCURRENCY { return std::result::Result::Err(request_error("hydration_concurrency")); } if role.as_str().is_empty() || role.as_str().trim() != role.as_str() { return std::result::Result::Err(request_error("role")); } if scope.kind() == crate::BackfillScopeKind::ExplicitSignatures { if min_context_slot.is_some() { return std::result::Result::Err(request_error("min_context_slot")); } let signature_count = match scope.signatures() { std::option::Option::Some(signatures) => signatures.len(), std::option::Option::None => return std::result::Result::Err(request_error("scope.signatures")), }; if signature_count > max_candidates { return std::result::Result::Err(request_error("max_candidates")); } } let scope_fingerprint = fingerprint_scope(&network, commitment, &scope, page_size, max_pages, max_candidates, min_context_slot); return std::result::Result::Ok(Self { job_id, network, role, commitment, scope, page_size, max_pages, max_candidates, hydration_concurrency, min_context_slot, scope_fingerprint, checkpoint: std::option::Option::None, }); } /// Returns the caller-owned logical Job identity. #[must_use] pub const fn job_id(&self) -> &ksp_job_api::JobId { return &self.job_id; } /// Returns the Store-scoped logical network identity for every candidate in this Job. #[must_use] pub const fn network(&self) -> &ksp_store_lib::RawNetworkId { return &self.network; } /// Returns the logical HTTP role used only for Transport selection. #[must_use] pub const fn role(&self) -> &ksp_onchain_transport_lib::HttpRoleName { return &self.role; } /// Returns the narrowed commitment used by discovery and hydration. #[must_use] pub const fn commitment(&self) -> crate::BackfillCommitment { return self.commitment; } /// Returns the validated discovery scope. #[must_use] pub const fn scope(&self) -> &crate::BackfillScope { return &self.scope; } /// Returns the per-request signature page size. #[must_use] pub const fn page_size(&self) -> usize { return self.page_size; } /// Returns the maximum number of address pages admitted by this Job. #[must_use] pub const fn max_pages(&self) -> usize { return self.max_pages; } /// Returns the maximum number of transaction candidates admitted by this Job. #[must_use] pub const fn max_candidates(&self) -> usize { return self.max_candidates; } /// Returns the future hydration concurrency bound. #[must_use] pub const fn hydration_concurrency(&self) -> usize { return self.hydration_concurrency; } /// Returns the optional Solana minimum context slot used by address discovery. #[must_use] pub const fn min_context_slot(&self) -> std::option::Option { return self.min_context_slot; } /// Returns the deterministic semantic scope fingerprint. /// /// Transport role, provider, endpoint and protocol are deliberately excluded. They describe /// acquisition provenance, not transaction or scope identity. #[must_use] pub const fn scope_fingerprint(&self) -> crate::BackfillScopeFingerprint { return self.scope_fingerprint; } /// Attaches one caller-owned checkpoint after validating Job and semantic scope identity. pub fn with_checkpoint(mut self, checkpoint: crate::BackfillCheckpoint) -> ksp_core_lib::Result { let validation = crate::validate_request_checkpoint(&self.job_id, self.scope_fingerprint, self.scope.kind(), &checkpoint); if let std::result::Result::Err(error) = validation { return std::result::Result::Err(error); } self.checkpoint = std::option::Option::Some(checkpoint); return std::result::Result::Ok(self); } /// Reissues a validated checkpoint onto a new Job lifecycle while preserving exact request semantics. pub fn resume_for_job(&self, job_id: ksp_job_api::JobId, checkpoint: crate::BackfillCheckpoint) -> ksp_core_lib::Result { let validation = crate::validate_request_checkpoint(&self.job_id, self.scope_fingerprint, self.scope.kind(), &checkpoint); if let std::result::Result::Err(error) = validation { return std::result::Result::Err(error); } let checkpoint = checkpoint.reissue_for_job(job_id.clone()); let mut resumed = self.clone(); resumed.job_id = job_id; resumed.checkpoint = std::option::Option::None; return resumed.with_checkpoint(checkpoint); } /// Returns the validated optional checkpoint supplied for controlled resumption. #[must_use] pub const fn checkpoint(&self) -> std::option::Option<&crate::BackfillCheckpoint> { return self.checkpoint.as_ref(); } } impl std::fmt::Debug for crate::BackfillRequest { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { return formatter .debug_struct("BackfillRequest") .field("job_id", &self.job_id) .field("network", &self.network) .field("role", &self.role) .field("commitment", &self.commitment) .field("scope", &self.scope) .field("page_size", &self.page_size) .field("max_pages", &self.max_pages) .field("max_candidates", &self.max_candidates) .field("hydration_concurrency", &self.hydration_concurrency) .field("min_context_slot", &self.min_context_slot) .field("scope_fingerprint", &self.scope_fingerprint) .field("has_checkpoint", &self.checkpoint.is_some()) .finish(); } } fn fingerprint_scope( network: &ksp_store_lib::RawNetworkId, commitment: crate::BackfillCommitment, scope: &crate::BackfillScope, page_size: usize, max_pages: usize, max_candidates: usize, min_context_slot: std::option::Option, ) -> crate::BackfillScopeFingerprint { let mut hasher = sha2::Sha256::new(); hasher.update(b"ksp.job.backfill.scope.v1\0"); hash_bytes(&mut hasher, network.as_str().as_bytes()); hash_bytes(&mut hasher, commitment.code().as_bytes()); hash_bytes(&mut hasher, scope.kind().code().as_bytes()); if let std::option::Option::Some(address) = scope.address() { hash_bytes(&mut hasher, address.as_ref()); } if let std::option::Option::Some(anchor) = scope.anchor() { hash_bytes(&mut hasher, anchor.as_str().as_bytes()); } if let std::option::Option::Some(signatures) = scope.signatures() { for signature in signatures { hash_bytes(&mut hasher, signature.as_str().as_bytes()); } } hash_u64(&mut hasher, page_size as u64); hash_u64(&mut hasher, max_pages as u64); hash_u64(&mut hasher, max_candidates as u64); match min_context_slot { std::option::Option::Some(slot) => { hasher.update([1_u8]); hash_u64(&mut hasher, slot); }, std::option::Option::None => hasher.update([0_u8]), } let bytes: [u8; 32] = hasher.finalize().into(); return crate::BackfillScopeFingerprint::from_bytes(bytes); } fn hash_bytes(hasher: &mut sha2::Sha256, value: &[u8]) { hash_u64(hasher, value.len() as u64); hasher.update(value); } fn hash_u64(hasher: &mut sha2::Sha256, value: u64) { hasher.update(value.to_be_bytes()); } fn request_error(field: &'static str) -> ksp_core_lib::Error { return ksp_core_lib::Error::new(crate::ERROR_CODE_BACKFILL_REQUEST_INVALID, "invalid bounded Backfill request").with_context("field", field); } fn signature_error() -> ksp_core_lib::Error { return ksp_core_lib::Error::new(crate::ERROR_CODE_BACKFILL_SIGNATURE_INVALID, "invalid bounded Backfill transaction signature") .with_context("field", "signature"); } fn valid_signature_text(value: &str) -> bool { if value.len() < crate::MIN_BACKFILL_SIGNATURE_TEXT_BYTES || value.len() > crate::MAX_BACKFILL_SIGNATURE_TEXT_BYTES { return false; } return value.bytes().all(|byte| { return matches!(byte, b'1'..=b'9' | b'A'..=b'H' | b'J'..=b'N' | b'P'..=b'Z' | b'a'..=b'k' | b'm'..=b'z'); }); } #[cfg(test)] #[path = "../unit_tests/request.rs"] mod tests;