299 lines
14 KiB
Rust
299 lines
14 KiB
Rust
// file: crates/ksp-job-backfill-lib/src/conversion.rs
|
|
// version: 5
|
|
|
|
use sha2::Digest; // rust-rules: trait-import
|
|
|
|
const RAW_TRANSACTION_METHOD_CODE: &str = "getTransaction";
|
|
const RAW_TRANSACTION_OBSERVATION_CONTRACT_VERSION: u32 = 1;
|
|
const RAW_TRANSACTION_PROTOCOL_CODE: &str = "solana.http.json_rpc";
|
|
|
|
/// Complete in-memory RAW transaction acquisition ready for Store persistence.
|
|
#[derive(Debug)]
|
|
pub struct BackfillRawAcquisition {
|
|
inner: Box<BackfillRawAcquisitionInner>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct BackfillRawAcquisitionInner {
|
|
acquisition: ksp_raw_transaction_lib::RawTransactionAcquisition,
|
|
}
|
|
|
|
impl crate::BackfillRawAcquisition {
|
|
/// Returns the canonical RAW transaction produced through the source-neutral common layer.
|
|
#[must_use]
|
|
pub fn transaction(&self) -> &ksp_store_lib::RawTransaction {
|
|
return self.inner.acquisition.transaction();
|
|
}
|
|
|
|
/// Returns the Backfill-owned acquisition observation linked by the common assembly layer.
|
|
#[must_use]
|
|
pub fn observation(&self) -> &ksp_store_lib::RawTransactionObservation {
|
|
return self.inner.acquisition.observation();
|
|
}
|
|
|
|
/// Consumes the in-memory acquisition into the canonical transaction and its Backfill-owned observation.
|
|
#[must_use]
|
|
pub fn into_parts(self) -> (ksp_store_lib::RawTransaction, ksp_store_lib::RawTransactionObservation) {
|
|
let inner = *self.inner;
|
|
return inner.acquisition.into_parts();
|
|
}
|
|
}
|
|
|
|
/// Result of hydrating one deterministic Backfill candidate through observed `getTransaction`.
|
|
#[derive(Debug)]
|
|
pub enum BackfillHydrationOutcome {
|
|
/// The RPC returned one complete transaction and common conversion produced canonical RAW plus Backfill provenance.
|
|
Available(crate::BackfillRawAcquisition),
|
|
/// The RPC returned JSON `null`; only the canonical transaction identity exists and no provenance is fabricated.
|
|
Missing(ksp_store_lib::RawTransactionReference),
|
|
}
|
|
|
|
impl crate::BackfillHydrationOutcome {
|
|
/// Returns the network-scoped transaction identity represented by this hydration outcome.
|
|
#[must_use]
|
|
pub fn reference(&self) -> &ksp_store_lib::RawTransactionReference {
|
|
return match self {
|
|
Self::Available(acquisition) => acquisition.transaction().reference(),
|
|
Self::Missing(reference) => reference,
|
|
};
|
|
}
|
|
|
|
/// Returns whether `getTransaction` returned JSON `null`.
|
|
#[must_use]
|
|
pub const fn is_missing(&self) -> bool {
|
|
return matches!(self, Self::Missing(_));
|
|
}
|
|
}
|
|
|
|
/// Hydrates one candidate with the typed observed Transport path and converts it through the common RAW v1 layer.
|
|
///
|
|
/// The caller supplies the local receipt timestamp because wall-clock ownership remains outside this
|
|
/// pure conversion path. Transport retains endpoint selection and retry. Backfill retains campaign
|
|
/// provenance and observation-key ownership; the common crate owns signature parsing, RAW v1
|
|
/// canonicalization, content hashing and transaction/observation assembly.
|
|
pub async fn hydrate_backfill_candidate(
|
|
transport: &ksp_onchain_transport_lib::HttpTransportPool,
|
|
request: &crate::BackfillRequest,
|
|
candidate: &crate::BackfillCandidate,
|
|
received_at: ksp_store_lib::RawTimestamp,
|
|
) -> ksp_core_lib::Result<crate::BackfillHydrationOutcome> {
|
|
let reference = canonical_reference(request, candidate);
|
|
let reference = match reference {
|
|
std::result::Result::Ok(reference) => reference,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let config = ksp_onchain_transport_lib::SolanaGetTransactionConfig::new(
|
|
std::option::Option::Some(request.commitment().transport()),
|
|
std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionEncoding::Base64),
|
|
std::option::Option::Some(0),
|
|
);
|
|
let observed = transport.get_transaction_observed(request.role(), candidate.identity().signature().as_str(), std::option::Option::Some(&config)).await;
|
|
let observed = match observed {
|
|
std::result::Result::Ok(observed) => observed,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let provider = observed.provider().as_str().to_owned();
|
|
let endpoint = observed.endpoint_name().to_owned();
|
|
let transaction = observed.into_value();
|
|
let transaction = match transaction {
|
|
std::option::Option::Some(transaction) => transaction,
|
|
std::option::Option::None => return std::result::Result::Ok(crate::BackfillHydrationOutcome::Missing(reference)),
|
|
};
|
|
let fields = CanonicalTransactionFields {
|
|
block_time: transaction.block_time(),
|
|
meta: transaction.meta(),
|
|
slot: transaction.slot(),
|
|
transaction: transaction.transaction(),
|
|
transaction_index: transaction.transaction_index(),
|
|
version: transaction.version(),
|
|
};
|
|
let acquisition = convert_available_fields(request, reference, fields, provider.as_str(), endpoint.as_str(), received_at);
|
|
return match acquisition {
|
|
std::result::Result::Ok(acquisition) => std::result::Result::Ok(crate::BackfillHydrationOutcome::Available(acquisition)),
|
|
std::result::Result::Err(error) => std::result::Result::Err(error),
|
|
};
|
|
}
|
|
|
|
/// Decodes one validated Base58 signature through the source-neutral common parser while preserving Backfill error semantics.
|
|
pub(crate) fn decode_backfill_signature(signature: &crate::BackfillSignature) -> ksp_core_lib::Result<ksp_store_lib::RawTransactionSignature> {
|
|
let parsed = ksp_raw_transaction_lib::parse_raw_transaction_signature(signature.as_str());
|
|
return match parsed {
|
|
std::result::Result::Ok(signature) => std::result::Result::Ok(signature),
|
|
std::result::Result::Err(_) => std::result::Result::Err(conversion_error("signature")),
|
|
};
|
|
}
|
|
|
|
struct CanonicalTransactionFields<'a> {
|
|
block_time: std::option::Option<i64>,
|
|
meta: &'a ksp_onchain_transport_lib::SolanaWireField<serde_json::Value>,
|
|
slot: u64,
|
|
transaction: &'a ksp_onchain_transport_lib::SolanaEncodedTransaction,
|
|
transaction_index: &'a ksp_onchain_transport_lib::SolanaWireField<u32>,
|
|
version: &'a ksp_onchain_transport_lib::SolanaWireField<ksp_onchain_transport_lib::SolanaTransactionVersion>,
|
|
}
|
|
|
|
fn canonical_reference(request: &crate::BackfillRequest, candidate: &crate::BackfillCandidate) -> ksp_core_lib::Result<ksp_store_lib::RawTransactionReference> {
|
|
if candidate.identity().network() != request.network() {
|
|
return std::result::Result::Err(conversion_error("candidate.network"));
|
|
}
|
|
let signature = crate::decode_backfill_signature(candidate.identity().signature());
|
|
let signature = match signature {
|
|
std::result::Result::Ok(signature) => signature,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
return std::result::Result::Ok(ksp_store_lib::RawTransactionReference::new(candidate.identity().network().clone(), signature));
|
|
}
|
|
|
|
fn convert_available_fields(
|
|
request: &crate::BackfillRequest,
|
|
reference: ksp_store_lib::RawTransactionReference,
|
|
fields: CanonicalTransactionFields<'_>,
|
|
provider: &str,
|
|
endpoint: &str,
|
|
received_at: ksp_store_lib::RawTimestamp,
|
|
) -> ksp_core_lib::Result<crate::BackfillRawAcquisition> {
|
|
let transaction = canonical_transaction(&reference, fields);
|
|
let transaction = match transaction {
|
|
std::result::Result::Ok(transaction) => transaction,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let provenance = build_provenance(request, provider, endpoint, received_at);
|
|
let provenance = match provenance {
|
|
std::result::Result::Ok(provenance) => provenance,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let observation_key = observation_key(request, transaction.reference(), provider, endpoint);
|
|
let acquisition = ksp_raw_transaction_lib::assemble_raw_transaction_acquisition(transaction, observation_key, provenance);
|
|
return std::result::Result::Ok(crate::BackfillRawAcquisition { inner: Box::new(BackfillRawAcquisitionInner { acquisition }) });
|
|
}
|
|
|
|
fn canonical_transaction(
|
|
reference: &ksp_store_lib::RawTransactionReference,
|
|
fields: CanonicalTransactionFields<'_>,
|
|
) -> ksp_core_lib::Result<ksp_store_lib::RawTransaction> {
|
|
let transaction_data = match fields.transaction {
|
|
ksp_onchain_transport_lib::SolanaEncodedTransaction::Binary { data, encoding }
|
|
if *encoding == ksp_onchain_transport_lib::SolanaTransactionBinaryEncoding::Base64 =>
|
|
{
|
|
data.clone()
|
|
},
|
|
ksp_onchain_transport_lib::SolanaEncodedTransaction::Binary { .. }
|
|
| ksp_onchain_transport_lib::SolanaEncodedTransaction::LegacyBinary(_)
|
|
| ksp_onchain_transport_lib::SolanaEncodedTransaction::Json(_) => {
|
|
return std::result::Result::Err(conversion_error("transaction.encoding"));
|
|
},
|
|
};
|
|
let meta = map_wire_field(fields.meta, |value| return value.clone());
|
|
let version = map_wire_field(fields.version, |value| {
|
|
return match value {
|
|
ksp_onchain_transport_lib::SolanaTransactionVersion::Legacy => ksp_raw_transaction_lib::RawTransactionVersion::Legacy,
|
|
ksp_onchain_transport_lib::SolanaTransactionVersion::Number(number) => ksp_raw_transaction_lib::RawTransactionVersion::Number(*number),
|
|
};
|
|
});
|
|
let transaction_index = map_wire_field(fields.transaction_index, |value| return *value);
|
|
let material = ksp_raw_transaction_lib::RawTransactionMaterial::binary_base64(
|
|
reference.network().clone(),
|
|
reference.signature(),
|
|
fields.slot,
|
|
fields.block_time,
|
|
transaction_data,
|
|
meta,
|
|
version,
|
|
transaction_index,
|
|
);
|
|
let canonical = ksp_raw_transaction_lib::canonicalize_raw_transaction(material);
|
|
return match canonical {
|
|
std::result::Result::Ok(transaction) => std::result::Result::Ok(transaction),
|
|
std::result::Result::Err(_) => std::result::Result::Err(conversion_error("common.canonicalization")),
|
|
};
|
|
}
|
|
|
|
fn map_wire_field<T, U, F>(field: &ksp_onchain_transport_lib::SolanaWireField<T>, mut map_value: F) -> ksp_raw_transaction_lib::RawTransactionWireField<U>
|
|
where
|
|
F: FnMut(&T) -> U,
|
|
{
|
|
return match field {
|
|
ksp_onchain_transport_lib::SolanaWireField::Omitted => ksp_raw_transaction_lib::RawTransactionWireField::Omitted,
|
|
ksp_onchain_transport_lib::SolanaWireField::Null => ksp_raw_transaction_lib::RawTransactionWireField::Null,
|
|
ksp_onchain_transport_lib::SolanaWireField::Value(value) => ksp_raw_transaction_lib::RawTransactionWireField::Value(map_value(value)),
|
|
};
|
|
}
|
|
|
|
fn build_provenance(
|
|
request: &crate::BackfillRequest,
|
|
provider: &str,
|
|
endpoint: &str,
|
|
received_at: ksp_store_lib::RawTimestamp,
|
|
) -> ksp_core_lib::Result<ksp_store_lib::RawAcquisitionProvenance> {
|
|
let provider = match provenance_code(provider, "provenance.provider") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let protocol = match provenance_code(RAW_TRANSACTION_PROTOCOL_CODE, "provenance.protocol") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let method = match provenance_code(RAW_TRANSACTION_METHOD_CODE, "provenance.method") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let endpoint = match provenance_code(endpoint, "provenance.endpoint") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let commitment = match provenance_code(request.commitment().code(), "provenance.commitment") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let capture_session = match provenance_code(request.job_id().as_str(), "provenance.capture_session") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let provenance = ksp_store_lib::RawAcquisitionProvenance::new(provider, protocol, method, ksp_store_lib::RawAcquisitionOrigin::Backfill, received_at)
|
|
.with_endpoint_id(endpoint)
|
|
.with_commitment(commitment)
|
|
.with_capture_session_id(capture_session);
|
|
return std::result::Result::Ok(provenance);
|
|
}
|
|
|
|
fn provenance_code(value: &str, field: &'static str) -> ksp_core_lib::Result<ksp_store_lib::RawProvenanceCode> {
|
|
let code = ksp_store_lib::RawProvenanceCode::new(value);
|
|
return match code {
|
|
std::result::Result::Ok(code) => std::result::Result::Ok(code),
|
|
std::result::Result::Err(_) => std::result::Result::Err(conversion_error(field)),
|
|
};
|
|
}
|
|
|
|
fn observation_key(
|
|
request: &crate::BackfillRequest,
|
|
reference: &ksp_store_lib::RawTransactionReference,
|
|
provider: &str,
|
|
endpoint: &str,
|
|
) -> ksp_store_lib::RawObservationKey {
|
|
let mut hasher = sha2::Sha256::new();
|
|
hasher.update(b"ksp.job.backfill.raw_transaction_observation.v1\0");
|
|
hash_bytes(&mut hasher, request.job_id().as_str().as_bytes());
|
|
hash_bytes(&mut hasher, request.scope_fingerprint().as_bytes());
|
|
hash_bytes(&mut hasher, reference.signature().as_bytes());
|
|
hash_bytes(&mut hasher, provider.as_bytes());
|
|
hash_bytes(&mut hasher, endpoint.as_bytes());
|
|
hash_bytes(&mut hasher, request.commitment().code().as_bytes());
|
|
hasher.update(RAW_TRANSACTION_OBSERVATION_CONTRACT_VERSION.to_be_bytes());
|
|
let bytes: [u8; 32] = hasher.finalize().into();
|
|
return ksp_store_lib::RawObservationKey::new(bytes);
|
|
}
|
|
|
|
fn hash_bytes(hasher: &mut sha2::Sha256, value: &[u8]) {
|
|
hasher.update((value.len() as u64).to_be_bytes());
|
|
hasher.update(value);
|
|
}
|
|
|
|
fn conversion_error(field: &'static str) -> ksp_core_lib::Error {
|
|
return ksp_core_lib::Error::new(crate::ERROR_CODE_BACKFILL_RAW_CONVERSION_INVALID, "invalid deterministic Backfill RAW conversion")
|
|
.with_context("field", field);
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "../unit_tests/conversion.rs"]
|
|
mod tests;
|