4544 lines
209 KiB
Rust
4544 lines
209 KiB
Rust
// file: crates/ksp-worker-raw-transaction-ingest-lib/unit_tests/runtime_resources.rs
|
|
// version: 35
|
|
|
|
fn grpc_endpoint(cluster: &str) -> std::option::Option<ksp_onchain_transport_lib::YellowstoneGrpcEndpointSettings> {
|
|
return grpc_endpoint_with_identity(cluster, "yellowstone-fixture", "fixture-provider");
|
|
}
|
|
|
|
fn grpc_endpoint_with_identity(
|
|
cluster: &str,
|
|
endpoint_name: &str,
|
|
provider: &str,
|
|
) -> std::option::Option<ksp_onchain_transport_lib::YellowstoneGrpcEndpointSettings> {
|
|
let url = match ksp_onchain_transport_lib::YellowstoneGrpcEndpointUrl::parse("http://127.0.0.1:10000/GRPC-SECRET-CANARY") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return std::option::Option::None,
|
|
};
|
|
return std::option::Option::Some(ksp_onchain_transport_lib::YellowstoneGrpcEndpointSettings::new(
|
|
endpoint_name,
|
|
true,
|
|
ksp_onchain_transport_lib::YellowstoneGrpcProviderName::new(provider),
|
|
ksp_onchain_transport_lib::YellowstoneGrpcClusterName::new(cluster),
|
|
url,
|
|
ksp_onchain_transport_lib::YellowstoneGrpcSessionSettings::default(),
|
|
));
|
|
}
|
|
|
|
fn http_pool(cluster: &str, role_name: &str, request_kind: &str) -> std::option::Option<ksp_onchain_transport_lib::HttpTransportPool> {
|
|
let url = match ksp_onchain_transport_lib::HttpEndpointUrl::parse("https://fixture.invalid/rpc?token=HTTP-SECRET-CANARY") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return std::option::Option::None,
|
|
};
|
|
let role = ksp_onchain_transport_lib::HttpEndpointRoleSettings::new(
|
|
ksp_onchain_transport_lib::HttpRoleName::new(role_name),
|
|
true,
|
|
std::vec![ksp_onchain_transport_lib::HttpRequestKind::new(request_kind)],
|
|
10,
|
|
ksp_onchain_transport_lib::HttpRoleLimits::new(
|
|
std::option::Option::None,
|
|
std::option::Option::None,
|
|
std::option::Option::None,
|
|
std::option::Option::None,
|
|
),
|
|
);
|
|
let endpoint = ksp_onchain_transport_lib::HttpEndpointSettings::new(
|
|
"http-fixture",
|
|
true,
|
|
ksp_onchain_transport_lib::HttpProviderName::new("fixture-provider"),
|
|
ksp_onchain_transport_lib::HttpClusterName::new(cluster),
|
|
url,
|
|
std::time::Duration::from_secs(1),
|
|
std::time::Duration::from_secs(2),
|
|
std::option::Option::Some(4),
|
|
std::vec![role],
|
|
);
|
|
let settings = ksp_onchain_transport_lib::HttpTransportSettings::new(
|
|
std::vec![endpoint],
|
|
ksp_onchain_transport_lib::HttpRetrySettings::new(1, std::time::Duration::from_millis(10), std::time::Duration::from_millis(20)),
|
|
);
|
|
return match ksp_onchain_transport_lib::HttpTransportPool::new(settings) {
|
|
std::result::Result::Ok(value) => std::option::Option::Some(value),
|
|
std::result::Result::Err(_) => std::option::Option::None,
|
|
};
|
|
}
|
|
|
|
fn http_polling_pool_with_identity(
|
|
cluster: &str,
|
|
role_name: &str,
|
|
endpoint_name: &str,
|
|
provider: &str,
|
|
request_kinds: &[&str],
|
|
) -> std::option::Option<ksp_onchain_transport_lib::HttpTransportPool> {
|
|
let url = match ksp_onchain_transport_lib::HttpEndpointUrl::parse("https://fixture.invalid/poll?token=HTTP-POLL-SECRET-CANARY") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return std::option::Option::None,
|
|
};
|
|
let role = ksp_onchain_transport_lib::HttpEndpointRoleSettings::new(
|
|
ksp_onchain_transport_lib::HttpRoleName::new(role_name),
|
|
true,
|
|
request_kinds.iter().map(|value| return ksp_onchain_transport_lib::HttpRequestKind::new(*value)).collect(),
|
|
10,
|
|
ksp_onchain_transport_lib::HttpRoleLimits::new(
|
|
std::option::Option::None,
|
|
std::option::Option::None,
|
|
std::option::Option::None,
|
|
std::option::Option::None,
|
|
),
|
|
);
|
|
let endpoint = ksp_onchain_transport_lib::HttpEndpointSettings::new(
|
|
endpoint_name,
|
|
true,
|
|
ksp_onchain_transport_lib::HttpProviderName::new(provider),
|
|
ksp_onchain_transport_lib::HttpClusterName::new(cluster),
|
|
url,
|
|
std::time::Duration::from_secs(1),
|
|
std::time::Duration::from_secs(2),
|
|
std::option::Option::Some(4),
|
|
std::vec![role],
|
|
);
|
|
let settings = ksp_onchain_transport_lib::HttpTransportSettings::new(
|
|
std::vec![endpoint],
|
|
ksp_onchain_transport_lib::HttpRetrySettings::new(1, std::time::Duration::from_millis(10), std::time::Duration::from_millis(20)),
|
|
);
|
|
return match ksp_onchain_transport_lib::HttpTransportPool::new(settings) {
|
|
std::result::Result::Ok(value) => std::option::Option::Some(value),
|
|
std::result::Result::Err(_) => std::option::Option::None,
|
|
};
|
|
}
|
|
|
|
fn http_polling_pool(cluster: &str) -> std::option::Option<ksp_onchain_transport_lib::HttpTransportPool> {
|
|
return http_polling_pool_with_identity(cluster, "live-poll", "poll-fixture", "fixture-provider", &["get_block", "get_blocks_with_limit", "get_slot"]);
|
|
}
|
|
|
|
fn ws_endpoint(
|
|
cluster: &str,
|
|
endpoint_name: &str,
|
|
provider: &str,
|
|
protocol: ksp_onchain_transport_lib::WsProtocolKind,
|
|
) -> std::option::Option<ksp_onchain_transport_lib::WsEndpointSettings> {
|
|
let capabilities = if protocol == ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard {
|
|
std::vec![
|
|
ksp_onchain_transport_lib::WsSubscriptionKind::Account,
|
|
ksp_onchain_transport_lib::WsSubscriptionKind::Block,
|
|
ksp_onchain_transport_lib::WsSubscriptionKind::Logs,
|
|
ksp_onchain_transport_lib::WsSubscriptionKind::Program,
|
|
ksp_onchain_transport_lib::WsSubscriptionKind::Root,
|
|
ksp_onchain_transport_lib::WsSubscriptionKind::Signature,
|
|
ksp_onchain_transport_lib::WsSubscriptionKind::Slot,
|
|
ksp_onchain_transport_lib::WsSubscriptionKind::SlotsUpdates,
|
|
ksp_onchain_transport_lib::WsSubscriptionKind::Vote,
|
|
]
|
|
} else if protocol == ksp_onchain_transport_lib::WsProtocolKind::HeliusLaserStream {
|
|
std::vec![
|
|
ksp_onchain_transport_lib::WsSubscriptionKind::Account,
|
|
ksp_onchain_transport_lib::WsSubscriptionKind::Logs,
|
|
ksp_onchain_transport_lib::WsSubscriptionKind::Program,
|
|
ksp_onchain_transport_lib::WsSubscriptionKind::Root,
|
|
ksp_onchain_transport_lib::WsSubscriptionKind::Signature,
|
|
ksp_onchain_transport_lib::WsSubscriptionKind::Slot,
|
|
ksp_onchain_transport_lib::WsSubscriptionKind::SlotsUpdates,
|
|
ksp_onchain_transport_lib::WsSubscriptionKind::HeliusTransaction,
|
|
]
|
|
} else {
|
|
return std::option::Option::None;
|
|
};
|
|
return ws_endpoint_with_capabilities(cluster, endpoint_name, provider, protocol, std::option::Option::Some(capabilities));
|
|
}
|
|
|
|
fn ws_endpoint_with_capabilities(
|
|
cluster: &str,
|
|
endpoint_name: &str,
|
|
provider: &str,
|
|
protocol: ksp_onchain_transport_lib::WsProtocolKind,
|
|
capabilities: std::option::Option<std::vec::Vec<ksp_onchain_transport_lib::WsSubscriptionKind>>,
|
|
) -> std::option::Option<ksp_onchain_transport_lib::WsEndpointSettings> {
|
|
let url = match ksp_onchain_transport_lib::WsEndpointUrl::parse("ws://127.0.0.1:10001/WS-SECRET-CANARY") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return std::option::Option::None,
|
|
};
|
|
let endpoint = ksp_onchain_transport_lib::WsEndpointSettings::new(
|
|
endpoint_name,
|
|
true,
|
|
ksp_onchain_transport_lib::WsProviderName::new(provider),
|
|
ksp_onchain_transport_lib::WsClusterName::new(cluster),
|
|
protocol,
|
|
url,
|
|
ksp_onchain_transport_lib::WsSessionSettings::default(),
|
|
);
|
|
return std::option::Option::Some(match capabilities {
|
|
std::option::Option::Some(value) => endpoint.with_subscription_capabilities(value),
|
|
std::option::Option::None => endpoint,
|
|
});
|
|
}
|
|
|
|
fn supervisor_contracts(
|
|
source_specs: &[(u8, &'static str, u8)],
|
|
) -> std::option::Option<std::sync::Arc<std::sync::Mutex<crate::RawTransactionIngestContinuityContracts>>> {
|
|
let network = match ksp_store_lib::RawNetworkId::new("devnet") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return std::option::Option::None,
|
|
};
|
|
let mut capabilities = std::vec::Vec::new();
|
|
for (source_key_byte, family, fingerprint_byte) in source_specs {
|
|
let (scope, reference_bearing, block_material) = if *family == "full_ledger" {
|
|
(crate::RawTransactionIngestCoverageScope::full_ledger_transactions(), false, true)
|
|
} else {
|
|
(crate::RawTransactionIngestCoverageScope::exact_source_scope(family, [*fingerprint_byte; 32]), true, false)
|
|
};
|
|
let capability = crate::RawTransactionIngestContinuityCapabilityDescriptor::new(
|
|
[*source_key_byte; 32],
|
|
network.clone(),
|
|
ksp_onchain_transport_lib::SolanaCommitment::Confirmed,
|
|
scope,
|
|
reference_bearing,
|
|
block_material,
|
|
false,
|
|
reference_bearing,
|
|
false,
|
|
false,
|
|
);
|
|
let capability = match capability {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return std::option::Option::None,
|
|
};
|
|
capabilities.push(capability);
|
|
}
|
|
return match crate::RawTransactionIngestContinuityContracts::new(capabilities) {
|
|
std::result::Result::Ok(value) => std::option::Option::Some(std::sync::Arc::new(std::sync::Mutex::new(value))),
|
|
std::result::Result::Err(_) => std::option::Option::None,
|
|
};
|
|
}
|
|
|
|
fn supervisor_inventory(
|
|
source_keys: std::vec::Vec<[u8; 32]>,
|
|
states: &[std::option::Option<crate::RawTransactionIngestSourceState>],
|
|
) -> std::option::Option<std::sync::Arc<std::sync::Mutex<super::RawTransactionIngestSourceInventory>>> {
|
|
if source_keys.len() != states.len() {
|
|
return std::option::Option::None;
|
|
}
|
|
let mut inventory = super::RawTransactionIngestSourceInventory::new(source_keys.clone());
|
|
for (index, (source_key, state)) in source_keys.iter().zip(states.iter()).enumerate() {
|
|
let projection = crate::RawTransactionIngestProcessingFrontierProjection::new(0, std::option::Option::Some(100), std::option::Option::None)
|
|
.with_source_continuity(*state, 0, 0, 0);
|
|
if inventory.update(index, *source_key, projection).is_err() {
|
|
return std::option::Option::None;
|
|
}
|
|
}
|
|
return std::option::Option::Some(std::sync::Arc::new(std::sync::Mutex::new(inventory)));
|
|
}
|
|
|
|
fn standard_block_source(
|
|
cluster: &str,
|
|
endpoint_name: &str,
|
|
filter: ksp_onchain_transport_lib::SolanaBlockSubscribeFilter,
|
|
commitment: ksp_onchain_transport_lib::SolanaCommitment,
|
|
) -> std::option::Option<crate::RawTransactionIngestStandardBlockSource> {
|
|
let endpoint = match ws_endpoint(cluster, endpoint_name, "fixture-ws-provider", ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::option::Option::None,
|
|
};
|
|
return match crate::RawTransactionIngestStandardBlockSource::new(endpoint, filter, commitment) {
|
|
std::result::Result::Ok(value) => std::option::Option::Some(value),
|
|
std::result::Result::Err(_) => std::option::Option::None,
|
|
};
|
|
}
|
|
|
|
fn standard_logs_source(
|
|
cluster: &str,
|
|
endpoint_name: &str,
|
|
filter: ksp_onchain_transport_lib::SolanaLogsSubscribeFilter,
|
|
commitment: ksp_onchain_transport_lib::SolanaCommitment,
|
|
hydration_role: &str,
|
|
) -> std::option::Option<crate::RawTransactionIngestStandardLogsSource> {
|
|
let endpoint = match ws_endpoint(cluster, endpoint_name, "fixture-ws-provider", ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::option::Option::None,
|
|
};
|
|
let pool = match http_pool(cluster, hydration_role, "get_transaction") {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::option::Option::None,
|
|
};
|
|
return match crate::RawTransactionIngestStandardLogsSource::new(
|
|
endpoint,
|
|
filter,
|
|
commitment,
|
|
pool,
|
|
ksp_onchain_transport_lib::HttpRoleName::new(hydration_role),
|
|
) {
|
|
std::result::Result::Ok(value) => std::option::Option::Some(value),
|
|
std::result::Result::Err(_) => std::option::Option::None,
|
|
};
|
|
}
|
|
|
|
fn helius_transaction_filter(failed: bool) -> ksp_onchain_transport_lib::HeliusTransactionSubscribeFilter {
|
|
return ksp_onchain_transport_lib::HeliusTransactionSubscribeFilter::new(
|
|
std::option::Option::Some(false),
|
|
std::option::Option::Some(failed),
|
|
std::option::Option::None,
|
|
std::option::Option::None,
|
|
std::option::Option::None,
|
|
std::option::Option::None,
|
|
std::option::Option::None,
|
|
);
|
|
}
|
|
|
|
fn helius_transaction_source(
|
|
cluster: &str,
|
|
endpoint_name: &str,
|
|
filter: ksp_onchain_transport_lib::HeliusTransactionSubscribeFilter,
|
|
commitment: ksp_onchain_transport_lib::SolanaCommitment,
|
|
hydration_role: &str,
|
|
) -> std::option::Option<crate::RawTransactionIngestHeliusTransactionSource> {
|
|
let endpoint = match ws_endpoint(cluster, endpoint_name, "fixture-helius-provider", ksp_onchain_transport_lib::WsProtocolKind::HeliusLaserStream) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::option::Option::None,
|
|
};
|
|
let pool = match http_pool(cluster, hydration_role, "get_transaction") {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::option::Option::None,
|
|
};
|
|
return match crate::RawTransactionIngestHeliusTransactionSource::new(
|
|
endpoint,
|
|
filter,
|
|
commitment,
|
|
pool,
|
|
ksp_onchain_transport_lib::HttpRoleName::new(hydration_role),
|
|
) {
|
|
std::result::Result::Ok(value) => std::option::Option::Some(value),
|
|
std::result::Result::Err(_) => std::option::Option::None,
|
|
};
|
|
}
|
|
|
|
struct HeliusTransactionViewFixture {
|
|
signature: std::string::String,
|
|
slot: u64,
|
|
transaction_index: u64,
|
|
}
|
|
|
|
impl super::RawTransactionIngestHeliusTransactionView for HeliusTransactionViewFixture {
|
|
fn signature(&self) -> &str {
|
|
return self.signature.as_str();
|
|
}
|
|
|
|
fn slot(&self) -> u64 {
|
|
return self.slot;
|
|
}
|
|
|
|
fn transaction_index(&self) -> u64 {
|
|
return self.transaction_index;
|
|
}
|
|
}
|
|
|
|
struct StandardLogsViewFixture {
|
|
signature: std::string::String,
|
|
slot: u64,
|
|
}
|
|
|
|
impl super::RawTransactionIngestStandardLogsView for StandardLogsViewFixture {
|
|
fn signature(&self) -> &str {
|
|
return self.signature.as_str();
|
|
}
|
|
|
|
fn slot(&self) -> u64 {
|
|
return self.slot;
|
|
}
|
|
}
|
|
|
|
fn transaction_request(
|
|
commitment: std::option::Option<ksp_onchain_transport_lib::SolanaCommitment>,
|
|
) -> std::option::Option<ksp_onchain_transport_lib::YellowstoneSubscribeRequest> {
|
|
let name = match ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("tx-fixture") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return std::option::Option::None,
|
|
};
|
|
let mut request = ksp_onchain_transport_lib::YellowstoneSubscribeRequest::new();
|
|
if request.insert_transaction_filter(name, ksp_onchain_transport_lib::YellowstoneSubscribeTransactionFilter::new()).is_err() {
|
|
return std::option::Option::None;
|
|
}
|
|
request.set_commitment(commitment);
|
|
return std::option::Option::Some(request);
|
|
}
|
|
|
|
struct SignalViewFixture {
|
|
created_at: std::option::Option<ksp_onchain_transport_lib::YellowstoneUpdateTimestamp>,
|
|
family: super::RawTransactionIngestSourceFamily,
|
|
filters: std::vec::Vec<ksp_onchain_transport_lib::YellowstoneSubscribeFilterName>,
|
|
index: u64,
|
|
signature: ksp_onchain_transport_lib::YellowstoneTransactionSignature,
|
|
slot: u64,
|
|
}
|
|
|
|
impl super::RawTransactionIngestYellowstoneSignalView for SignalViewFixture {
|
|
fn created_at(&self) -> std::option::Option<ksp_onchain_transport_lib::YellowstoneUpdateTimestamp> {
|
|
return self.created_at;
|
|
}
|
|
|
|
fn family(&self) -> super::RawTransactionIngestSourceFamily {
|
|
return self.family;
|
|
}
|
|
|
|
fn filters(&self) -> &[ksp_onchain_transport_lib::YellowstoneSubscribeFilterName] {
|
|
return self.filters.as_slice();
|
|
}
|
|
|
|
fn index(&self) -> u64 {
|
|
return self.index;
|
|
}
|
|
|
|
fn signature(&self) -> ksp_onchain_transport_lib::YellowstoneTransactionSignature {
|
|
return self.signature;
|
|
}
|
|
|
|
fn slot(&self) -> u64 {
|
|
return self.slot;
|
|
}
|
|
}
|
|
|
|
struct BlockViewFixture {
|
|
created_at: std::option::Option<ksp_onchain_transport_lib::YellowstoneUpdateTimestamp>,
|
|
filters: std::vec::Vec<ksp_onchain_transport_lib::YellowstoneSubscribeFilterName>,
|
|
slot: u64,
|
|
transactions: std::vec::Vec<(ksp_onchain_transport_lib::YellowstoneTransactionSignature, u64)>,
|
|
}
|
|
|
|
impl super::RawTransactionIngestYellowstoneBlockView for BlockViewFixture {
|
|
fn created_at(&self) -> std::option::Option<ksp_onchain_transport_lib::YellowstoneUpdateTimestamp> {
|
|
return self.created_at;
|
|
}
|
|
|
|
fn filters(&self) -> &[ksp_onchain_transport_lib::YellowstoneSubscribeFilterName] {
|
|
return self.filters.as_slice();
|
|
}
|
|
|
|
fn slot(&self) -> u64 {
|
|
return self.slot;
|
|
}
|
|
|
|
fn transaction_count(&self) -> usize {
|
|
return self.transactions.len();
|
|
}
|
|
|
|
fn transaction_identity(&self, position: usize) -> ksp_core_lib::Result<(ksp_onchain_transport_lib::YellowstoneTransactionSignature, u64)> {
|
|
return match self.transactions.get(position) {
|
|
std::option::Option::Some(value) => std::result::Result::Ok(*value),
|
|
std::option::Option::None => std::result::Result::Err(crate::runtime_error("fixture.block_transaction_missing")),
|
|
};
|
|
}
|
|
}
|
|
|
|
struct ContinuityViewFixture {
|
|
created_at: std::option::Option<ksp_onchain_transport_lib::YellowstoneUpdateTimestamp>,
|
|
family: super::RawTransactionIngestContinuityFamily,
|
|
filters: std::vec::Vec<ksp_onchain_transport_lib::YellowstoneSubscribeFilterName>,
|
|
parent_slot: std::option::Option<u64>,
|
|
slot: u64,
|
|
status: std::option::Option<super::RawTransactionIngestContinuityStatus>,
|
|
}
|
|
|
|
impl super::RawTransactionIngestYellowstoneContinuityView for ContinuityViewFixture {
|
|
fn created_at(&self) -> std::option::Option<ksp_onchain_transport_lib::YellowstoneUpdateTimestamp> {
|
|
return self.created_at;
|
|
}
|
|
|
|
fn family(&self) -> super::RawTransactionIngestContinuityFamily {
|
|
return self.family;
|
|
}
|
|
|
|
fn filters(&self) -> &[ksp_onchain_transport_lib::YellowstoneSubscribeFilterName] {
|
|
return self.filters.as_slice();
|
|
}
|
|
|
|
fn parent_slot(&self) -> std::option::Option<u64> {
|
|
return self.parent_slot;
|
|
}
|
|
|
|
fn slot(&self) -> u64 {
|
|
return self.slot;
|
|
}
|
|
|
|
fn status(&self) -> std::option::Option<super::RawTransactionIngestContinuityStatus> {
|
|
return self.status;
|
|
}
|
|
}
|
|
|
|
fn filter_names(values: &[&str]) -> std::option::Option<std::vec::Vec<ksp_onchain_transport_lib::YellowstoneSubscribeFilterName>> {
|
|
let mut filters = std::vec::Vec::with_capacity(values.len());
|
|
for value in values {
|
|
let filter = match ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new(*value) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return std::option::Option::None,
|
|
};
|
|
filters.push(filter);
|
|
}
|
|
return std::option::Option::Some(filters);
|
|
}
|
|
|
|
fn source_with_identity(
|
|
cluster: &str,
|
|
endpoint_name: &str,
|
|
filter_name: &str,
|
|
failed: std::option::Option<bool>,
|
|
) -> std::option::Option<crate::RawTransactionIngestYellowstoneSource> {
|
|
return source_with_identity_and_hydration_role(cluster, endpoint_name, filter_name, failed, "hydration");
|
|
}
|
|
|
|
fn source_with_identity_and_hydration_role(
|
|
cluster: &str,
|
|
endpoint_name: &str,
|
|
filter_name: &str,
|
|
failed: std::option::Option<bool>,
|
|
hydration_role: &str,
|
|
) -> std::option::Option<crate::RawTransactionIngestYellowstoneSource> {
|
|
let endpoint = match grpc_endpoint_with_identity(cluster, endpoint_name, "fixture-provider") {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::option::Option::None,
|
|
};
|
|
let channel = match ksp_onchain_transport_lib::YellowstoneGrpcChannel::prepare(&endpoint) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return std::option::Option::None,
|
|
};
|
|
let name = match ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new(filter_name) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return std::option::Option::None,
|
|
};
|
|
let mut filter = ksp_onchain_transport_lib::YellowstoneSubscribeTransactionFilter::new();
|
|
filter.set_failed(failed);
|
|
let mut request = ksp_onchain_transport_lib::YellowstoneSubscribeRequest::new();
|
|
if request.insert_transaction_filter(name, filter).is_err() {
|
|
return std::option::Option::None;
|
|
}
|
|
request.set_commitment(std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed));
|
|
let pool = match http_pool(cluster, hydration_role, "get_transaction") {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::option::Option::None,
|
|
};
|
|
return match crate::RawTransactionIngestYellowstoneSource::new(channel, request, pool, ksp_onchain_transport_lib::HttpRoleName::new(hydration_role)) {
|
|
std::result::Result::Ok(value) => std::option::Option::Some(value),
|
|
std::result::Result::Err(_) => std::option::Option::None,
|
|
};
|
|
}
|
|
|
|
fn signal_source() -> std::option::Option<crate::RawTransactionIngestYellowstoneSource> {
|
|
let endpoint = match grpc_endpoint("devnet") {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::option::Option::None,
|
|
};
|
|
let channel = match ksp_onchain_transport_lib::YellowstoneGrpcChannel::prepare(&endpoint) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return std::option::Option::None,
|
|
};
|
|
let request = match transaction_request(std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed)) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::option::Option::None,
|
|
};
|
|
let pool = match http_pool("devnet", "hydration", "get_transaction") {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::option::Option::None,
|
|
};
|
|
return match crate::RawTransactionIngestYellowstoneSource::new(channel, request, pool, ksp_onchain_transport_lib::HttpRoleName::new("hydration")) {
|
|
std::result::Result::Ok(value) => std::option::Option::Some(value),
|
|
std::result::Result::Err(_) => std::option::Option::None,
|
|
};
|
|
}
|
|
|
|
const PRE_004_OTHER_TRANSACTION_BASE64: &str = "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEA";
|
|
const PRE_004_ZERO_SIGNATURE_TEXT: &str = "1111111111111111111111111111111111111111111111111111111111111111";
|
|
const PRE_004_ZERO_TRANSACTION_BASE64: &str = "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
|
|
|
fn http_pool_for_url(url: &str, cluster: &str, endpoint_name: &str, provider: &str) -> std::option::Option<ksp_onchain_transport_lib::HttpTransportPool> {
|
|
return http_pool_for_url_with_request_kinds(url, cluster, endpoint_name, provider, &["get_transaction"]);
|
|
}
|
|
|
|
fn http_pool_for_url_with_request_kinds(
|
|
url: &str,
|
|
cluster: &str,
|
|
endpoint_name: &str,
|
|
provider: &str,
|
|
request_kinds: &[&str],
|
|
) -> std::option::Option<ksp_onchain_transport_lib::HttpTransportPool> {
|
|
let url = match ksp_onchain_transport_lib::HttpEndpointUrl::parse(url) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return std::option::Option::None,
|
|
};
|
|
let role = ksp_onchain_transport_lib::HttpEndpointRoleSettings::new(
|
|
ksp_onchain_transport_lib::HttpRoleName::new("hydration"),
|
|
true,
|
|
request_kinds.iter().map(|value| return ksp_onchain_transport_lib::HttpRequestKind::new(*value)).collect(),
|
|
10,
|
|
ksp_onchain_transport_lib::HttpRoleLimits::new(
|
|
std::option::Option::None,
|
|
std::option::Option::None,
|
|
std::option::Option::None,
|
|
std::option::Option::None,
|
|
),
|
|
);
|
|
let endpoint = ksp_onchain_transport_lib::HttpEndpointSettings::new(
|
|
endpoint_name,
|
|
true,
|
|
ksp_onchain_transport_lib::HttpProviderName::new(provider),
|
|
ksp_onchain_transport_lib::HttpClusterName::new(cluster),
|
|
url,
|
|
std::time::Duration::from_secs(1),
|
|
std::time::Duration::from_secs(1),
|
|
std::option::Option::Some(1),
|
|
std::vec![role],
|
|
);
|
|
let settings = ksp_onchain_transport_lib::HttpTransportSettings::new(
|
|
std::vec![endpoint],
|
|
ksp_onchain_transport_lib::HttpRetrySettings::new(0, std::time::Duration::from_millis(1), std::time::Duration::from_millis(1)),
|
|
);
|
|
return match ksp_onchain_transport_lib::HttpTransportPool::new(settings) {
|
|
std::result::Result::Ok(value) => std::option::Option::Some(value),
|
|
std::result::Result::Err(_) => std::option::Option::None,
|
|
};
|
|
}
|
|
|
|
fn signal_source_for_http_url(url: &str) -> std::option::Option<crate::RawTransactionIngestYellowstoneSource> {
|
|
return signal_source_for_http_url_with_commitment(url, ksp_onchain_transport_lib::SolanaCommitment::Confirmed);
|
|
}
|
|
|
|
fn signal_source_for_http_url_with_commitment(
|
|
url: &str,
|
|
commitment: ksp_onchain_transport_lib::SolanaCommitment,
|
|
) -> std::option::Option<crate::RawTransactionIngestYellowstoneSource> {
|
|
let endpoint = match grpc_endpoint("devnet") {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::option::Option::None,
|
|
};
|
|
let channel = match ksp_onchain_transport_lib::YellowstoneGrpcChannel::prepare(&endpoint) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return std::option::Option::None,
|
|
};
|
|
let request = match transaction_request(std::option::Option::Some(commitment)) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::option::Option::None,
|
|
};
|
|
let pool = match http_pool_for_url(url, "devnet", "http-hydration-fixture", "fixture-http-provider") {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::option::Option::None,
|
|
};
|
|
return match crate::RawTransactionIngestYellowstoneSource::new(channel, request, pool, ksp_onchain_transport_lib::HttpRoleName::new("hydration")) {
|
|
std::result::Result::Ok(value) => std::option::Option::Some(value),
|
|
std::result::Result::Err(_) => std::option::Option::None,
|
|
};
|
|
}
|
|
|
|
fn signal_source_for_http_url_with_request_kinds(url: &str, request_kinds: &[&str]) -> std::option::Option<crate::RawTransactionIngestYellowstoneSource> {
|
|
let endpoint = match grpc_endpoint("devnet") {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::option::Option::None,
|
|
};
|
|
let channel = match ksp_onchain_transport_lib::YellowstoneGrpcChannel::prepare(&endpoint) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return std::option::Option::None,
|
|
};
|
|
let request = match transaction_request(std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed)) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::option::Option::None,
|
|
};
|
|
let pool = match http_pool_for_url_with_request_kinds(url, "devnet", "http-hydration-fixture", "fixture-http-provider", request_kinds) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::option::Option::None,
|
|
};
|
|
return match crate::RawTransactionIngestYellowstoneSource::new(channel, request, pool, ksp_onchain_transport_lib::HttpRoleName::new("hydration")) {
|
|
std::result::Result::Ok(value) => std::option::Option::Some(value),
|
|
std::result::Result::Err(_) => std::option::Option::None,
|
|
};
|
|
}
|
|
|
|
fn standard_logs_source_for_http_url(
|
|
url: &str,
|
|
filter: ksp_onchain_transport_lib::SolanaLogsSubscribeFilter,
|
|
) -> std::option::Option<crate::RawTransactionIngestStandardLogsSource> {
|
|
let endpoint = match ws_endpoint("devnet", "standard-logs-fixture", "fixture-ws-provider", ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::option::Option::None,
|
|
};
|
|
let pool = match http_pool_for_url(url, "devnet", "http-hydration-fixture", "fixture-http-provider") {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::option::Option::None,
|
|
};
|
|
return match crate::RawTransactionIngestStandardLogsSource::new(
|
|
endpoint,
|
|
filter,
|
|
ksp_onchain_transport_lib::SolanaCommitment::Confirmed,
|
|
pool,
|
|
ksp_onchain_transport_lib::HttpRoleName::new("hydration"),
|
|
) {
|
|
std::result::Result::Ok(value) => std::option::Option::Some(value),
|
|
std::result::Result::Err(_) => std::option::Option::None,
|
|
};
|
|
}
|
|
|
|
fn helius_transaction_source_for_http_url(url: &str) -> std::option::Option<crate::RawTransactionIngestHeliusTransactionSource> {
|
|
let endpoint =
|
|
match ws_endpoint("devnet", "helius-transaction-fixture", "fixture-helius-provider", ksp_onchain_transport_lib::WsProtocolKind::HeliusLaserStream) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::option::Option::None,
|
|
};
|
|
let pool = match http_pool_for_url(url, "devnet", "http-hydration-fixture", "fixture-http-provider") {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::option::Option::None,
|
|
};
|
|
return match crate::RawTransactionIngestHeliusTransactionSource::new(
|
|
endpoint,
|
|
helius_transaction_filter(false),
|
|
ksp_onchain_transport_lib::SolanaCommitment::Confirmed,
|
|
pool,
|
|
ksp_onchain_transport_lib::HttpRoleName::new("hydration"),
|
|
) {
|
|
std::result::Result::Ok(value) => std::option::Option::Some(value),
|
|
std::result::Result::Err(_) => std::option::Option::None,
|
|
};
|
|
}
|
|
|
|
fn pre_004_settings() -> std::option::Option<crate::RawTransactionIngestSettings> {
|
|
let network = match ksp_store_lib::RawNetworkId::new("devnet") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return std::option::Option::None,
|
|
};
|
|
let worker_id = match ksp_worker_api::WorkerId::new("worker-fixture") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return std::option::Option::None,
|
|
};
|
|
return std::option::Option::Some(crate::RawTransactionIngestSettings::with_defaults(network, worker_id));
|
|
}
|
|
|
|
fn pre_004_signal(
|
|
source: &crate::RawTransactionIngestYellowstoneSource,
|
|
family: super::RawTransactionIngestSourceFamily,
|
|
slot: u64,
|
|
index: u64,
|
|
filters: &[&str],
|
|
signature_byte: u8,
|
|
) -> std::option::Option<super::RawTransactionIngestSourceSignal> {
|
|
let filters = match filter_names(filters) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::option::Option::None,
|
|
};
|
|
let created_at = match ksp_onchain_transport_lib::YellowstoneUpdateTimestamp::new(1_760_000_123, 456_000_000) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return std::option::Option::None,
|
|
};
|
|
let fixture = SignalViewFixture {
|
|
created_at: std::option::Option::Some(created_at),
|
|
family,
|
|
filters,
|
|
index,
|
|
signature: ksp_onchain_transport_lib::YellowstoneTransactionSignature::new([signature_byte; 64]),
|
|
slot,
|
|
};
|
|
return std::option::Option::Some(super::project_yellowstone_signal(source, &fixture));
|
|
}
|
|
|
|
fn serve_http_once(body: std::string::String) -> std::io::Result<(std::string::String, std::thread::JoinHandle<std::io::Result<std::string::String>>)> {
|
|
let listener = match std::net::TcpListener::bind("127.0.0.1:0") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let address = match listener.local_addr() {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let handle = std::thread::spawn(move || {
|
|
let (mut stream, _) = match listener.accept() {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let request = match read_http_request(&mut stream) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let response =
|
|
std::format!("HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", body.len(), body,);
|
|
if let std::result::Result::Err(error) = std::io::Write::write_all(&mut stream, response.as_bytes()) {
|
|
return std::result::Result::Err(error);
|
|
}
|
|
return std::result::Result::Ok(request);
|
|
});
|
|
return std::result::Result::Ok((std::format!("http://{address}"), handle));
|
|
}
|
|
|
|
fn read_http_request(stream: &mut std::net::TcpStream) -> std::io::Result<std::string::String> {
|
|
let mut bytes = std::vec::Vec::new();
|
|
let mut buffer = [0_u8; 1024];
|
|
loop {
|
|
let count = match std::io::Read::read(stream, &mut buffer) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
if count == 0 {
|
|
break;
|
|
}
|
|
bytes.extend_from_slice(&buffer[..count]);
|
|
if http_request_complete(bytes.as_slice()) {
|
|
break;
|
|
}
|
|
}
|
|
return match std::string::String::from_utf8(bytes) {
|
|
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
|
std::result::Result::Err(_) => std::result::Result::Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "fixture HTTP request is not UTF-8")),
|
|
};
|
|
}
|
|
|
|
fn http_request_complete(bytes: &[u8]) -> bool {
|
|
let text = match std::str::from_utf8(bytes) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return false,
|
|
};
|
|
let header_end = match text.find("\r\n\r\n") {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return false,
|
|
};
|
|
let mut content_length = 0_usize;
|
|
for line in text[..header_end].lines() {
|
|
let (name, value) = match line.split_once(':') {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => continue,
|
|
};
|
|
if name.eq_ignore_ascii_case("content-length") {
|
|
content_length = match value.trim().parse::<usize>() {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return false,
|
|
};
|
|
}
|
|
}
|
|
return bytes.len() >= header_end.saturating_add(4).saturating_add(content_length);
|
|
}
|
|
|
|
fn http_request_body(request: &str) -> &str {
|
|
return match request.split_once("\r\n\r\n") {
|
|
std::option::Option::Some((_, body)) => body,
|
|
std::option::Option::None => "",
|
|
};
|
|
}
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn pre_002_source_accepts_matching_confirmed_transaction_and_get_transaction_route_without_io() {
|
|
let endpoint = match grpc_endpoint("devnet") {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let channel = match ksp_onchain_transport_lib::YellowstoneGrpcChannel::prepare(&endpoint) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let request = match transaction_request(std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed)) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let pool = match http_pool("devnet", "hydration", "get_transaction") {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let source = crate::RawTransactionIngestYellowstoneSource::new(channel, request, pool, ksp_onchain_transport_lib::HttpRoleName::new("hydration"));
|
|
assert!(source.is_ok());
|
|
return;
|
|
}
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn pre_002_source_rejects_processed_and_implicit_commitment() {
|
|
for commitment in [std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Processed), std::option::Option::None] {
|
|
let endpoint = match grpc_endpoint("devnet") {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let channel = match ksp_onchain_transport_lib::YellowstoneGrpcChannel::prepare(&endpoint) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let request = match transaction_request(commitment) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let pool = match http_pool("devnet", "hydration", "get_transaction") {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let source = crate::RawTransactionIngestYellowstoneSource::new(channel, request, pool, ksp_onchain_transport_lib::HttpRoleName::new("hydration"));
|
|
let error = match source {
|
|
std::result::Result::Ok(_) => return,
|
|
std::result::Result::Err(error) => error,
|
|
};
|
|
assert_eq!(error.code(), crate::ERROR_CODE_RAW_TRANSACTION_INGEST_RUNTIME_INVALID);
|
|
assert!(error.context().iter().any(|context| return context.value() == "runtime_resources.hydration_commitment_invalid"));
|
|
}
|
|
return;
|
|
}
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn pre_002_source_requires_ingestion_bearing_filter_family() {
|
|
let endpoint = match grpc_endpoint("devnet") {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let channel = match ksp_onchain_transport_lib::YellowstoneGrpcChannel::prepare(&endpoint) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let mut request = ksp_onchain_transport_lib::YellowstoneSubscribeRequest::new();
|
|
request.set_commitment(std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Finalized));
|
|
let pool = match http_pool("devnet", "hydration", "get_transaction") {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let source = crate::RawTransactionIngestYellowstoneSource::new(channel, request, pool, ksp_onchain_transport_lib::HttpRoleName::new("hydration"));
|
|
let error = match source {
|
|
std::result::Result::Ok(_) => return,
|
|
std::result::Result::Err(error) => error,
|
|
};
|
|
assert_eq!(error.code(), crate::ERROR_CODE_RAW_TRANSACTION_INGEST_RUNTIME_INVALID);
|
|
assert!(error.context().iter().any(|context| return context.value() == "runtime_resources.ingestion_filter_missing"));
|
|
return;
|
|
}
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn pre_002_source_rejects_missing_hydration_capability_and_cross_transport_network() {
|
|
for (http_cluster, request_kind, expected_condition) in [
|
|
("devnet", "get_balance", "runtime_resources.hydration_role_unsupported"),
|
|
("mainnet", "get_transaction", "runtime_resources.transport_network_mismatch"),
|
|
] {
|
|
let endpoint = match grpc_endpoint("devnet") {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let channel = match ksp_onchain_transport_lib::YellowstoneGrpcChannel::prepare(&endpoint) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let request = match transaction_request(std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed)) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let pool = match http_pool(http_cluster, "hydration", request_kind) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let source = crate::RawTransactionIngestYellowstoneSource::new(channel, request, pool, ksp_onchain_transport_lib::HttpRoleName::new("hydration"));
|
|
let error = match source {
|
|
std::result::Result::Ok(_) => return,
|
|
std::result::Result::Err(error) => error,
|
|
};
|
|
assert_eq!(error.code(), crate::ERROR_CODE_RAW_TRANSACTION_INGEST_RUNTIME_INVALID);
|
|
assert!(error.context().iter().any(|context| return context.value() == expected_condition));
|
|
}
|
|
return;
|
|
}
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn pre_002_runtime_resource_debug_is_safe_and_exposes_no_client_inner() {
|
|
let endpoint = match grpc_endpoint("devnet") {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let channel = match ksp_onchain_transport_lib::YellowstoneGrpcChannel::prepare(&endpoint) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let request = match transaction_request(std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed)) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let pool = match http_pool("devnet", "hydration", "get_transaction") {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let source = match crate::RawTransactionIngestYellowstoneSource::new(channel, request, pool, ksp_onchain_transport_lib::HttpRoleName::new("hydration")) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let resources = crate::RawTransactionIngestRuntimeResources::new(source);
|
|
let debug = std::format!("{resources:?}");
|
|
assert!(debug.contains("RawTransactionIngestRuntimeResources"));
|
|
assert!(debug.contains("source_count"));
|
|
assert!(debug.contains("1"));
|
|
assert!(!debug.contains("devnet"));
|
|
assert!(!debug.contains("blocks_meta_filter_count"));
|
|
assert!(!debug.contains("slot_filter_count"));
|
|
assert!(!debug.contains("GRPC-SECRET-CANARY"));
|
|
assert!(!debug.contains("HTTP-SECRET-CANARY"));
|
|
assert!(!debug.contains("127.0.0.1"));
|
|
assert!(!debug.contains("fixture.invalid"));
|
|
assert!(!debug.contains("tx-fixture"));
|
|
return;
|
|
}
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn pre_002_runtime_resources_reject_worker_network_mismatch_without_starting_source() {
|
|
let endpoint = match grpc_endpoint("devnet") {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let channel = match ksp_onchain_transport_lib::YellowstoneGrpcChannel::prepare(&endpoint) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let request = match transaction_request(std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed)) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let pool = match http_pool("devnet", "hydration", "get_transaction") {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let source = match crate::RawTransactionIngestYellowstoneSource::new(channel, request, pool, ksp_onchain_transport_lib::HttpRoleName::new("hydration")) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let resources = crate::RawTransactionIngestRuntimeResources::new(source);
|
|
let network = match ksp_store_lib::RawNetworkId::new("mainnet") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let error = match resources.validate_network(&network) {
|
|
std::result::Result::Ok(()) => return,
|
|
std::result::Result::Err(error) => error,
|
|
};
|
|
assert_eq!(error.code(), crate::ERROR_CODE_RAW_TRANSACTION_INGEST_RUNTIME_INVALID);
|
|
assert!(error.context().iter().any(|context| return context.value() == "runtime_resources.worker_network_mismatch"));
|
|
return;
|
|
}
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn v0_3_13_pre_002_live_source_key_is_stable_and_request_sensitive() {
|
|
let first = match source_with_identity("devnet", "source-a", "tx-a", std::option::Option::Some(false)) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let same = match source_with_identity("devnet", "source-a", "tx-a", std::option::Option::Some(false)) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let changed_filter = match source_with_identity("devnet", "source-a", "tx-a", std::option::Option::Some(true)) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let changed_endpoint = match source_with_identity("devnet", "source-b", "tx-a", std::option::Option::Some(false)) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let changed_hydration_role = match source_with_identity_and_hydration_role("devnet", "source-a", "tx-a", std::option::Option::Some(false), "secondary") {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
assert_eq!(first.source_key, same.source_key);
|
|
assert_eq!(first.source_key, changed_hydration_role.source_key);
|
|
assert_ne!(first.source_key, changed_filter.source_key);
|
|
assert_ne!(first.source_key, changed_endpoint.source_key);
|
|
return;
|
|
}
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn v0_3_13_pre_002_runtime_resources_accept_exact_bound_and_reject_excess_transactionally() {
|
|
let first = match source_with_identity("devnet", "source-00", "tx", std::option::Option::None) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let mut resources = crate::RawTransactionIngestRuntimeResources::new(first);
|
|
for index in 1..crate::MAX_RAW_TRANSACTION_INGEST_LIVE_SOURCES {
|
|
let endpoint_name = std::format!("source-{index:02}");
|
|
let source = match source_with_identity("devnet", endpoint_name.as_str(), "tx", std::option::Option::None) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
assert!(resources.try_push_yellowstone_source(source).is_ok());
|
|
}
|
|
assert_eq!(resources.source_count(), crate::MAX_RAW_TRANSACTION_INGEST_LIVE_SOURCES);
|
|
let excess = match source_with_identity("devnet", "source-excess", "tx", std::option::Option::None) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let error = match resources.try_push_yellowstone_source(excess) {
|
|
std::result::Result::Ok(()) => return,
|
|
std::result::Result::Err(error) => error,
|
|
};
|
|
assert_eq!(error.code(), crate::ERROR_CODE_RAW_TRANSACTION_INGEST_RUNTIME_INVALID);
|
|
assert!(error.context().iter().any(|context| return context.value() == "runtime_resources.source_count_exceeded"));
|
|
assert_eq!(resources.source_count(), crate::MAX_RAW_TRANSACTION_INGEST_LIVE_SOURCES);
|
|
return;
|
|
}
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn v0_3_13_pre_002_runtime_resources_reject_duplicate_and_cross_network_source_before_activation() {
|
|
let first = match source_with_identity("devnet", "source-a", "tx", std::option::Option::None) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let duplicate = match source_with_identity("devnet", "source-a", "tx", std::option::Option::None) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let other_network = match source_with_identity("mainnet", "source-b", "tx", std::option::Option::None) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let mut resources = crate::RawTransactionIngestRuntimeResources::new(first);
|
|
let duplicate_error = match resources.try_push_yellowstone_source(duplicate) {
|
|
std::result::Result::Ok(()) => return,
|
|
std::result::Result::Err(error) => error,
|
|
};
|
|
assert!(duplicate_error.context().iter().any(|context| return context.value() == "runtime_resources.duplicate_source_identity"));
|
|
assert_eq!(resources.source_count(), 1);
|
|
let network_error = match resources.try_push_yellowstone_source(other_network) {
|
|
std::result::Result::Ok(()) => return,
|
|
std::result::Result::Err(error) => error,
|
|
};
|
|
assert!(network_error.context().iter().any(|context| return context.value() == "runtime_resources.source_network_mismatch"));
|
|
assert_eq!(resources.source_count(), 1);
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn v0_3_13_pre_007_source_inventory_aggregates_frontier_and_lifecycle_conservatively() {
|
|
let source_keys = std::vec![[1_u8; 32], [2_u8; 32]];
|
|
let continuity_contracts = match supervisor_contracts(&[(1, "fixture-a", 1), (2, "fixture-b", 2)]) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let inventory = std::sync::Arc::new(std::sync::Mutex::new(super::RawTransactionIngestSourceInventory::new(source_keys)));
|
|
let (aggregate_sender, aggregate_receiver) = tokio::sync::watch::channel(crate::RawTransactionIngestProcessingFrontierProjection::empty());
|
|
let first = super::RawTransactionIngestSourceInventoryPublisher {
|
|
aggregate_sender: aggregate_sender.clone(),
|
|
continuity_contracts: std::sync::Arc::clone(&continuity_contracts),
|
|
entry_index: 0,
|
|
inventory: std::sync::Arc::clone(&inventory),
|
|
source_key: [1_u8; 32],
|
|
};
|
|
let second =
|
|
super::RawTransactionIngestSourceInventoryPublisher { aggregate_sender, continuity_contracts, entry_index: 1, inventory, source_key: [2_u8; 32] };
|
|
assert!(
|
|
first
|
|
.publish(
|
|
crate::RawTransactionIngestProcessingFrontierProjection::new(1, std::option::Option::Some(50), std::option::Option::Some(45))
|
|
.with_source_continuity(std::option::Option::Some(crate::RawTransactionIngestSourceState::Active), 1, 2, 0),
|
|
)
|
|
.is_ok()
|
|
);
|
|
let first_aggregate = aggregate_receiver.borrow().clone();
|
|
assert_eq!(first_aggregate.hydration_pending(), 1);
|
|
assert_eq!(first_aggregate.processing_frontier_slot(), std::option::Option::None);
|
|
assert_eq!(first_aggregate.oldest_pending_slot(), std::option::Option::Some(45));
|
|
assert_eq!(first_aggregate.source_state(), std::option::Option::Some(crate::RawTransactionIngestSourceState::Active));
|
|
assert_eq!(first_aggregate.source_total(), 2);
|
|
assert_eq!(first_aggregate.source_active(), 1);
|
|
assert_eq!(first_aggregate.source_reconnecting(), 0);
|
|
assert_eq!(first_aggregate.source_failed(), 0);
|
|
assert!(first_aggregate.continuity_policy_observed());
|
|
assert!(!first_aggregate.future_target_coverage());
|
|
assert!(
|
|
second
|
|
.publish(
|
|
crate::RawTransactionIngestProcessingFrontierProjection::new(2, std::option::Option::Some(42), std::option::Option::Some(40))
|
|
.with_source_continuity(std::option::Option::Some(crate::RawTransactionIngestSourceState::Reconnecting), 3, 4, 1),
|
|
)
|
|
.is_ok()
|
|
);
|
|
let aggregate = aggregate_receiver.borrow().clone();
|
|
assert_eq!(aggregate.hydration_pending(), 3);
|
|
assert_eq!(aggregate.processing_frontier_slot(), std::option::Option::Some(42));
|
|
assert_eq!(aggregate.oldest_pending_slot(), std::option::Option::Some(40));
|
|
assert_eq!(aggregate.source_state(), std::option::Option::Some(crate::RawTransactionIngestSourceState::Reconnecting));
|
|
assert_eq!(aggregate.source_total(), 2);
|
|
assert_eq!(aggregate.source_active(), 1);
|
|
assert_eq!(aggregate.source_reconnecting(), 1);
|
|
assert_eq!(aggregate.source_failed(), 0);
|
|
assert_eq!(aggregate.source_reconnect_total(), 4);
|
|
assert_eq!(aggregate.source_replay_attempt_total(), 6);
|
|
assert_eq!(aggregate.source_continuity_gap_total(), 1);
|
|
assert!(aggregate.continuity_policy_observed());
|
|
assert!(!aggregate.continuity_has_open_gaps());
|
|
assert!(!aggregate.future_target_coverage());
|
|
return;
|
|
}
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn v0_3_13_pre_007_source_supervisor_joins_all_children_on_stop() {
|
|
let active = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
|
let (stop_sender, stop_receiver) = tokio::sync::watch::channel(false);
|
|
let (source_stop_sender, source_stop_receiver) = tokio::sync::watch::channel(false);
|
|
let source_keys = std::vec![[1_u8; 32], [2_u8; 32], [3_u8; 32]];
|
|
let contracts = match supervisor_contracts(&[(1, "fixture-a", 1), (2, "fixture-b", 2), (3, "fixture-c", 3)]) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let inventory = match supervisor_inventory(
|
|
source_keys.clone(),
|
|
&[
|
|
std::option::Option::Some(crate::RawTransactionIngestSourceState::Active),
|
|
std::option::Option::Some(crate::RawTransactionIngestSourceState::Active),
|
|
std::option::Option::Some(crate::RawTransactionIngestSourceState::Active),
|
|
],
|
|
) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let mut children = tokio::task::JoinSet::new();
|
|
for source_key in source_keys {
|
|
let active = std::sync::Arc::clone(&active);
|
|
let mut source_stop_receiver = source_stop_receiver.clone();
|
|
let _abort_handle = children.spawn(async move {
|
|
active.fetch_add(1, std::sync::atomic::Ordering::AcqRel);
|
|
loop {
|
|
let changed = source_stop_receiver.changed().await;
|
|
if changed.is_err() || *source_stop_receiver.borrow() {
|
|
active.fetch_sub(1, std::sync::atomic::Ordering::AcqRel);
|
|
return (source_key, std::result::Result::Ok(()));
|
|
}
|
|
}
|
|
});
|
|
}
|
|
let supervisor = tokio::spawn(async move {
|
|
return super::supervise_live_source_tasks(
|
|
stop_receiver,
|
|
source_stop_sender,
|
|
children,
|
|
contracts,
|
|
inventory,
|
|
tokio::sync::watch::channel(crate::RawTransactionIngestProcessingFrontierProjection::empty()).0,
|
|
std::time::Duration::from_secs(5),
|
|
)
|
|
.await;
|
|
});
|
|
for _ in 0..64 {
|
|
if active.load(std::sync::atomic::Ordering::Acquire) == 3 {
|
|
break;
|
|
}
|
|
tokio::task::yield_now().await;
|
|
}
|
|
assert_eq!(active.load(std::sync::atomic::Ordering::Acquire), 3);
|
|
stop_sender.send_replace(true);
|
|
let joined = supervisor.await;
|
|
let result = match joined {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
assert!(result.is_ok());
|
|
assert_eq!(active.load(std::sync::atomic::Ordering::Acquire), 0);
|
|
return;
|
|
}
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn v0_3_13_pre_007_source_failure_stops_and_joins_sibling_sources() {
|
|
let barrier = std::sync::Arc::new(tokio::sync::Barrier::new(2));
|
|
let sibling_active = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
|
|
let (_stop_sender, stop_receiver) = tokio::sync::watch::channel(false);
|
|
let (source_stop_sender, source_stop_receiver) = tokio::sync::watch::channel(false);
|
|
let contracts = match supervisor_contracts(&[(1, "fixture-a", 1), (2, "fixture-b", 2)]) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let inventory = match supervisor_inventory(
|
|
std::vec![[1_u8; 32], [2_u8; 32]],
|
|
&[
|
|
std::option::Option::Some(crate::RawTransactionIngestSourceState::Failed),
|
|
std::option::Option::Some(crate::RawTransactionIngestSourceState::Active),
|
|
],
|
|
) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let mut children = tokio::task::JoinSet::new();
|
|
let failing_barrier = std::sync::Arc::clone(&barrier);
|
|
let _failing_abort_handle = children.spawn(async move {
|
|
let _barrier_wait = failing_barrier.wait().await;
|
|
return ([1_u8; 32], std::result::Result::Err(crate::runtime_error("test.pre_007_source_failed")));
|
|
});
|
|
let sibling_barrier = std::sync::Arc::clone(&barrier);
|
|
let sibling_active_for_task = std::sync::Arc::clone(&sibling_active);
|
|
let mut sibling_stop_receiver = source_stop_receiver.clone();
|
|
let _sibling_abort_handle = children.spawn(async move {
|
|
sibling_active_for_task.store(true, std::sync::atomic::Ordering::Release);
|
|
let _barrier_wait = sibling_barrier.wait().await;
|
|
loop {
|
|
let changed = sibling_stop_receiver.changed().await;
|
|
if changed.is_err() || *sibling_stop_receiver.borrow() {
|
|
sibling_active_for_task.store(false, std::sync::atomic::Ordering::Release);
|
|
return ([2_u8; 32], std::result::Result::Ok(()));
|
|
}
|
|
}
|
|
});
|
|
let result = super::supervise_live_source_tasks(
|
|
stop_receiver,
|
|
source_stop_sender,
|
|
children,
|
|
contracts,
|
|
inventory,
|
|
tokio::sync::watch::channel(crate::RawTransactionIngestProcessingFrontierProjection::empty()).0,
|
|
std::time::Duration::from_secs(5),
|
|
)
|
|
.await;
|
|
assert!(result.is_err());
|
|
assert!(!sibling_active.load(std::sync::atomic::Ordering::Acquire));
|
|
return;
|
|
}
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn pre_003_transaction_signal_fixture_preserves_exact_source_neutral_identity() {
|
|
let source = match signal_source() {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let filters = match filter_names(&["filter-b", "filter-a", "filter-a"]) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let created_at = match ksp_onchain_transport_lib::YellowstoneUpdateTimestamp::new(1_760_000_123, 456_000_000) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let fixture = SignalViewFixture {
|
|
created_at: std::option::Option::Some(created_at),
|
|
family: super::RawTransactionIngestSourceFamily::Transaction,
|
|
filters,
|
|
index: 7,
|
|
signature: ksp_onchain_transport_lib::YellowstoneTransactionSignature::new([0xAB; 64]),
|
|
slot: 42,
|
|
};
|
|
let signal = super::project_yellowstone_signal(&source, &fixture);
|
|
assert_eq!(signal.family, super::RawTransactionIngestSourceFamily::Transaction);
|
|
assert_eq!(signal.network.as_str(), "devnet");
|
|
assert_eq!(signal.signature.as_bytes(), &[0xAB; 64]);
|
|
assert_eq!(signal.slot, 42);
|
|
assert_eq!(signal.transaction_index, std::option::Option::Some(7));
|
|
assert_eq!(signal.route.provider.as_str(), "fixture-provider");
|
|
assert_eq!(signal.route.endpoint_id.as_str(), "yellowstone-fixture");
|
|
assert_eq!(signal.matched_filter_count, 3);
|
|
assert_eq!(
|
|
signal.matched_filter_fingerprint,
|
|
[
|
|
0x20, 0x83, 0x1d, 0xc8, 0x37, 0x8b, 0x60, 0x70, 0xf9, 0x1d, 0x9d, 0xe6, 0xd3, 0xa1, 0xd2, 0xb3, 0xd7, 0x72, 0x98, 0xe3, 0xad, 0xa7, 0x6c, 0x01,
|
|
0xfa, 0xa3, 0x08, 0x26, 0xff, 0xab, 0xcd, 0x84,
|
|
]
|
|
);
|
|
assert_eq!(signal.created_at, std::option::Option::Some(super::RawTransactionIngestSourceTimestamp { nanos: 456_000_000, seconds: 1_760_000_123 }));
|
|
return;
|
|
}
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn pre_003_transaction_status_signal_has_no_payload_or_remote_error_material() {
|
|
let source = match signal_source() {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let filters = match filter_names(&["status-secret-canary"]) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let fixture = SignalViewFixture {
|
|
created_at: std::option::Option::None,
|
|
family: super::RawTransactionIngestSourceFamily::TransactionStatus,
|
|
filters,
|
|
index: 9,
|
|
signature: ksp_onchain_transport_lib::YellowstoneTransactionSignature::new([0xCD; 64]),
|
|
slot: 84,
|
|
};
|
|
let signal = super::project_yellowstone_signal(&source, &fixture);
|
|
assert_eq!(signal.family, super::RawTransactionIngestSourceFamily::TransactionStatus);
|
|
assert_eq!(signal.signature.as_bytes(), &[0xCD; 64]);
|
|
assert_eq!(signal.slot, 84);
|
|
assert_eq!(signal.transaction_index, std::option::Option::Some(9));
|
|
assert_eq!(signal.created_at, std::option::Option::None);
|
|
let debug = std::format!("{signal:?}");
|
|
assert!(!debug.contains("status-secret-canary"));
|
|
assert!(debug.contains("signature_bytes"));
|
|
assert!(!debug.contains("YellowstoneTransactionError"));
|
|
assert!(!debug.contains("transaction_body"));
|
|
assert!(!debug.contains("transaction_meta"));
|
|
return;
|
|
}
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn pre_003_filter_fingerprint_is_order_and_duplicate_insensitive() {
|
|
let source = match signal_source() {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let first_filters = match filter_names(&["filter-b", "filter-a", "filter-a"]) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let second_filters = match filter_names(&["filter-a", "filter-b"]) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let first = SignalViewFixture {
|
|
created_at: std::option::Option::None,
|
|
family: super::RawTransactionIngestSourceFamily::Transaction,
|
|
filters: first_filters,
|
|
index: 1,
|
|
signature: ksp_onchain_transport_lib::YellowstoneTransactionSignature::new([1; 64]),
|
|
slot: 1,
|
|
};
|
|
let second = SignalViewFixture {
|
|
created_at: std::option::Option::None,
|
|
family: super::RawTransactionIngestSourceFamily::Transaction,
|
|
filters: second_filters,
|
|
index: 1,
|
|
signature: ksp_onchain_transport_lib::YellowstoneTransactionSignature::new([1; 64]),
|
|
slot: 1,
|
|
};
|
|
let first_signal = super::project_yellowstone_signal(&source, &first);
|
|
let second_signal = super::project_yellowstone_signal(&source, &second);
|
|
assert_eq!(first_signal.matched_filter_fingerprint, second_signal.matched_filter_fingerprint);
|
|
assert_eq!(first_signal.matched_filter_count, 3);
|
|
assert_eq!(second_signal.matched_filter_count, 2);
|
|
return;
|
|
}
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn pre_003_source_rejects_route_identity_that_cannot_become_safe_provenance() {
|
|
for (endpoint_name, provider, expected_condition) in [
|
|
("yellowstone-fixture", "fixture/provider", "runtime_resources.yellowstone_provider_unrepresentable"),
|
|
("yellowstone/fixture", "fixture-provider", "runtime_resources.yellowstone_endpoint_unrepresentable"),
|
|
] {
|
|
let endpoint = match grpc_endpoint_with_identity("devnet", endpoint_name, provider) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let channel = match ksp_onchain_transport_lib::YellowstoneGrpcChannel::prepare(&endpoint) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let request = match transaction_request(std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed)) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let pool = match http_pool("devnet", "hydration", "get_transaction") {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let result = crate::RawTransactionIngestYellowstoneSource::new(channel, request, pool, ksp_onchain_transport_lib::HttpRoleName::new("hydration"));
|
|
let error = match result {
|
|
std::result::Result::Ok(_) => return,
|
|
std::result::Result::Err(error) => error,
|
|
};
|
|
assert_eq!(error.code(), crate::ERROR_CODE_RAW_TRANSACTION_INGEST_RUNTIME_INVALID);
|
|
assert!(error.context().iter().any(|context| return context.value() == expected_condition));
|
|
}
|
|
return;
|
|
}
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn pre_005_block_projects_transaction_signals_in_exact_source_order() {
|
|
let source = match signal_source() {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let filters = match filter_names(&["block-filter-b", "block-filter-a"]) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let created_at = match ksp_onchain_transport_lib::YellowstoneUpdateTimestamp::new(1_760_000_321, 123_000_000) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let fixture = BlockViewFixture {
|
|
created_at: std::option::Option::Some(created_at),
|
|
filters,
|
|
slot: 777,
|
|
transactions: std::vec![
|
|
(ksp_onchain_transport_lib::YellowstoneTransactionSignature::new([0x11; 64]), 9),
|
|
(ksp_onchain_transport_lib::YellowstoneTransactionSignature::new([0x22; 64]), 3),
|
|
(ksp_onchain_transport_lib::YellowstoneTransactionSignature::new([0x33; 64]), 17),
|
|
],
|
|
};
|
|
let signals = match super::project_yellowstone_block_signals(&source, &fixture) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
assert_eq!(signals.len(), 3);
|
|
assert_eq!(signals[0].signature.as_bytes(), &[0x11; 64]);
|
|
assert_eq!(signals[1].signature.as_bytes(), &[0x22; 64]);
|
|
assert_eq!(signals[2].signature.as_bytes(), &[0x33; 64]);
|
|
assert_eq!(signals[0].transaction_index, std::option::Option::Some(9));
|
|
assert_eq!(signals[1].transaction_index, std::option::Option::Some(3));
|
|
assert_eq!(signals[2].transaction_index, std::option::Option::Some(17));
|
|
for signal in &signals {
|
|
assert_eq!(signal.family, super::RawTransactionIngestSourceFamily::Block);
|
|
assert_eq!(signal.slot, 777);
|
|
assert_eq!(signal.network.as_str(), "devnet");
|
|
assert_eq!(signal.matched_filter_count, 2);
|
|
assert_eq!(signal.created_at, std::option::Option::Some(super::RawTransactionIngestSourceTimestamp { nanos: 123_000_000, seconds: 1_760_000_321 }));
|
|
}
|
|
return;
|
|
}
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn pre_005_block_hydration_uses_distinct_composite_method() {
|
|
let source = match signal_source() {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let settings = match pre_004_settings() {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let signal = match pre_004_signal(&source, super::RawTransactionIngestSourceFamily::Block, 42, 7, &["block-filter"], 0) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let received_at = match ksp_store_lib::RawTimestamp::from_unix_millis(1_760_000_124_000) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let hydration = source.hydration_context();
|
|
let provenance = match super::build_hydration_provenance(
|
|
&hydration,
|
|
&settings,
|
|
&signal,
|
|
"fixture-http-provider",
|
|
"http-hydration-fixture",
|
|
ksp_onchain_transport_lib::SolanaCommitment::Confirmed,
|
|
received_at,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
assert_eq!(provenance.acquisition_method().as_str(), "block_get_transaction");
|
|
return;
|
|
}
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn pre_005_block_meta_and_slot_project_continuity_only_without_remote_dead_error() {
|
|
let source = match signal_source() {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let block_filters = match filter_names(&["block-meta-filter"]) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let slot_filters = match filter_names(&["slot-filter-secret", "slot-filter-secret"]) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let block_meta = ContinuityViewFixture {
|
|
created_at: std::option::Option::None,
|
|
family: super::RawTransactionIngestContinuityFamily::BlockMeta,
|
|
filters: block_filters,
|
|
parent_slot: std::option::Option::Some(899),
|
|
slot: 900,
|
|
status: std::option::Option::None,
|
|
};
|
|
let slot = ContinuityViewFixture {
|
|
created_at: std::option::Option::None,
|
|
family: super::RawTransactionIngestContinuityFamily::Slot,
|
|
filters: slot_filters,
|
|
parent_slot: std::option::Option::Some(900),
|
|
slot: 901,
|
|
status: std::option::Option::Some(super::RawTransactionIngestContinuityStatus::Dead),
|
|
};
|
|
let block_meta_signal = super::project_yellowstone_continuity_signal(&source, &block_meta);
|
|
let slot_signal = super::project_yellowstone_continuity_signal(&source, &slot);
|
|
assert_eq!(block_meta_signal.family, super::RawTransactionIngestContinuityFamily::BlockMeta);
|
|
assert_eq!(block_meta_signal.slot, 900);
|
|
assert_eq!(block_meta_signal.parent_slot, std::option::Option::Some(899));
|
|
assert_eq!(block_meta_signal.status, std::option::Option::None);
|
|
assert_eq!(slot_signal.family, super::RawTransactionIngestContinuityFamily::Slot);
|
|
assert_eq!(slot_signal.slot, 901);
|
|
assert_eq!(slot_signal.parent_slot, std::option::Option::Some(900));
|
|
assert_eq!(slot_signal.status, std::option::Option::Some(super::RawTransactionIngestContinuityStatus::Dead));
|
|
assert_eq!(slot_signal.matched_filter_count, 2);
|
|
let debug = std::format!("{slot_signal:?}");
|
|
assert!(!debug.contains("slot-filter-secret"));
|
|
assert!(!debug.contains("dead_error"));
|
|
assert!(!debug.contains("transaction"));
|
|
assert!(!debug.contains("meta"));
|
|
assert!(!debug.contains("payload"));
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn pre_005_slot_status_projection_is_exact_and_source_neutral() {
|
|
for (source, expected) in [
|
|
(ksp_onchain_transport_lib::YellowstoneSlotStatus::Processed, super::RawTransactionIngestContinuityStatus::Processed),
|
|
(ksp_onchain_transport_lib::YellowstoneSlotStatus::Confirmed, super::RawTransactionIngestContinuityStatus::Confirmed),
|
|
(ksp_onchain_transport_lib::YellowstoneSlotStatus::Finalized, super::RawTransactionIngestContinuityStatus::Finalized),
|
|
(ksp_onchain_transport_lib::YellowstoneSlotStatus::FirstShredReceived, super::RawTransactionIngestContinuityStatus::FirstShredReceived),
|
|
(ksp_onchain_transport_lib::YellowstoneSlotStatus::Completed, super::RawTransactionIngestContinuityStatus::Completed),
|
|
(ksp_onchain_transport_lib::YellowstoneSlotStatus::CreatedBank, super::RawTransactionIngestContinuityStatus::CreatedBank),
|
|
(ksp_onchain_transport_lib::YellowstoneSlotStatus::Dead, super::RawTransactionIngestContinuityStatus::Dead),
|
|
] {
|
|
assert_eq!(super::map_yellowstone_slot_status(source), expected);
|
|
}
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn pre_004_composite_provenance_rejects_overflow_without_truncation() {
|
|
let provider = match ksp_store_lib::RawProvenanceCode::new("yellowstone-provider") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let endpoint_id = match ksp_store_lib::RawProvenanceCode::new("yellowstone-endpoint") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let route = super::RawTransactionIngestSourceRoute { endpoint_id, provider };
|
|
let result = super::composite_provenance_codes(&route, "ys", "h".repeat(120).as_str(), "http-endpoint");
|
|
let error = match result {
|
|
std::result::Result::Ok(_) => return,
|
|
std::result::Result::Err(error) => error,
|
|
};
|
|
assert_eq!(error.code(), crate::ERROR_CODE_RAW_TRANSACTION_INGEST_RUNTIME_INVALID);
|
|
assert!(error.context().iter().any(|context| return context.value() == "runtime_resources.composite_provider_unrepresentable"));
|
|
return;
|
|
}
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn pre_004_observed_get_transaction_closes_signal_to_common_raw_ingress_with_composite_provenance() {
|
|
let body = std::format!(
|
|
"{{\"jsonrpc\":\"2.0\",\"result\":{{\"slot\":42,\"blockTime\":1760000120,\"transaction\":[\"{}\",\"base64\"],\"meta\":{{\"err\":null,\"fee\":5000}},\"version\":\"legacy\",\"transactionIndex\":7}},\"id\":1}}",
|
|
PRE_004_ZERO_TRANSACTION_BASE64,
|
|
);
|
|
let (url, server) = match serve_http_once(body) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let source = match signal_source_for_http_url(url.as_str()) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let settings = match pre_004_settings() {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let signal = match pre_004_signal(&source, super::RawTransactionIngestSourceFamily::Transaction, 42, 7, &["tx-fixture"], 0) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let received_at = match ksp_store_lib::RawTimestamp::from_unix_millis(1_760_000_124_000) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let outcome = super::hydrate_yellowstone_signal(&source, &settings, signal, received_at).await;
|
|
assert!(outcome.is_ok());
|
|
let outcome = match outcome {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let request = match server.join() {
|
|
std::result::Result::Ok(std::result::Result::Ok(value)) => value,
|
|
_ => return,
|
|
};
|
|
let request_body = http_request_body(request.as_str());
|
|
assert!(request_body.contains("\"method\":\"getTransaction\""));
|
|
assert!(request_body.contains(PRE_004_ZERO_SIGNATURE_TEXT));
|
|
assert!(request_body.contains("\"commitment\":\"confirmed\""));
|
|
assert!(request_body.contains("\"encoding\":\"base64\""));
|
|
assert!(request_body.contains("\"maxSupportedTransactionVersion\":1"));
|
|
let ingress = match outcome {
|
|
super::RawTransactionIngestHydrationOutcome::Available(value) => value,
|
|
super::RawTransactionIngestHydrationOutcome::Missing(_) => return,
|
|
};
|
|
assert_eq!(ingress.provenance.provider().as_str(), "ys.fixture-provider:http.fixture-http-provider");
|
|
assert_eq!(ingress.provenance.protocol().as_str(), "yellowstone_http");
|
|
assert_eq!(ingress.provenance.acquisition_method().as_str(), "transaction_get_transaction");
|
|
assert_eq!(ingress.provenance.origin(), ksp_store_lib::RawAcquisitionOrigin::Live);
|
|
assert_eq!(ingress.provenance.received_at(), received_at);
|
|
assert_eq!(
|
|
ingress.provenance.endpoint_id().map(ksp_store_lib::RawProvenanceCode::as_str),
|
|
std::option::Option::Some("ys.yellowstone-fixture:http.http-hydration-fixture"),
|
|
);
|
|
assert_eq!(ingress.provenance.commitment().map(ksp_store_lib::RawProvenanceCode::as_str), std::option::Option::Some("confirmed"));
|
|
assert_eq!(ingress.provenance.capture_session_id().map(ksp_store_lib::RawProvenanceCode::as_str), std::option::Option::Some("worker-fixture"));
|
|
assert_eq!(ingress.provenance.filter_id().map(ksp_store_lib::RawProvenanceCode::as_str), std::option::Option::Some("tx-fixture"));
|
|
assert_eq!(ingress.provenance.observed_at().map(|value| return value.unix_millis()), std::option::Option::Some(1_760_000_123_456));
|
|
let (mut admission, sender) = crate::RawTransactionAdmission::new(1);
|
|
if sender.send(*ingress).await.is_err() {
|
|
return;
|
|
}
|
|
std::mem::drop(sender);
|
|
let acquisition = match admission.receive(settings.network()).await {
|
|
std::result::Result::Ok(std::option::Option::Some(value)) => value,
|
|
_ => return,
|
|
};
|
|
assert_eq!(acquisition.transaction().reference().network().as_str(), "devnet");
|
|
assert_eq!(acquisition.transaction().reference().signature().as_bytes(), &[0_u8; 64]);
|
|
assert_eq!(acquisition.transaction().slot(), 42);
|
|
assert_eq!(acquisition.transaction().block_time().map(|value| return value.unix_millis()), std::option::Option::Some(1_760_000_120_000),);
|
|
assert_eq!(acquisition.observation().provenance().provider().as_str(), "ys.fixture-provider:http.fixture-http-provider");
|
|
return;
|
|
}
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn pre_004_finalized_hydration_preserves_request_and_provenance_commitment() {
|
|
let body = std::format!(
|
|
"{{\"jsonrpc\":\"2.0\",\"result\":{{\"slot\":42,\"blockTime\":1760000120,\"transaction\":[\"{}\",\"base64\"],\"meta\":null,\"version\":\"legacy\",\"transactionIndex\":7}},\"id\":1}}",
|
|
PRE_004_ZERO_TRANSACTION_BASE64,
|
|
);
|
|
let (url, server) = match serve_http_once(body) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let source = match signal_source_for_http_url_with_commitment(url.as_str(), ksp_onchain_transport_lib::SolanaCommitment::Finalized) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let settings = match pre_004_settings() {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let signal = match pre_004_signal(&source, super::RawTransactionIngestSourceFamily::Transaction, 42, 7, &["tx-fixture"], 0) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let received_at = match ksp_store_lib::RawTimestamp::from_unix_millis(1_760_000_124_000) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let outcome = match super::hydrate_yellowstone_signal(&source, &settings, signal, received_at).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let request = match server.join() {
|
|
std::result::Result::Ok(std::result::Result::Ok(value)) => value,
|
|
_ => return,
|
|
};
|
|
let request_body = http_request_body(request.as_str());
|
|
assert!(request_body.contains("\"commitment\":\"finalized\""));
|
|
assert!(request_body.contains("\"encoding\":\"base64\""));
|
|
assert!(request_body.contains("\"maxSupportedTransactionVersion\":1"));
|
|
let ingress = match outcome {
|
|
super::RawTransactionIngestHydrationOutcome::Available(value) => value,
|
|
super::RawTransactionIngestHydrationOutcome::Missing(_) => return,
|
|
};
|
|
assert_eq!(ingress.provenance.commitment().map(ksp_store_lib::RawProvenanceCode::as_str), std::option::Option::Some("finalized"));
|
|
return;
|
|
}
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn pre_004_get_transaction_null_is_missing_without_ingress_or_worker_retry() {
|
|
let (url, server) = match serve_http_once("{\"jsonrpc\":\"2.0\",\"result\":null,\"id\":1}".to_owned()) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let source = match signal_source_for_http_url(url.as_str()) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let settings = match pre_004_settings() {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let signal = match pre_004_signal(&source, super::RawTransactionIngestSourceFamily::TransactionStatus, 42, 7, &["status-fixture"], 0) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let received_at = match ksp_store_lib::RawTimestamp::from_unix_millis(1_760_000_124_000) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let outcome = match super::hydrate_yellowstone_signal(&source, &settings, signal, received_at).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let request = match server.join() {
|
|
std::result::Result::Ok(std::result::Result::Ok(value)) => value,
|
|
_ => return,
|
|
};
|
|
assert!(http_request_body(request.as_str()).contains(PRE_004_ZERO_SIGNATURE_TEXT));
|
|
let reference = match outcome {
|
|
super::RawTransactionIngestHydrationOutcome::Available(_) => return,
|
|
super::RawTransactionIngestHydrationOutcome::Missing(value) => value,
|
|
};
|
|
assert_eq!(reference.network().as_str(), "devnet");
|
|
assert_eq!(reference.signature().as_bytes(), &[0_u8; 64]);
|
|
return;
|
|
}
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn pre_004_v0_null_and_omitted_wire_fields_preserve_common_raw_semantics() {
|
|
let body = std::format!(
|
|
"{{\"jsonrpc\":\"2.0\",\"result\":{{\"slot\":42,\"blockTime\":null,\"transaction\":[\"{}\",\"base64\"],\"meta\":null,\"version\":0}},\"id\":1}}",
|
|
PRE_004_ZERO_TRANSACTION_BASE64,
|
|
);
|
|
let (url, server) = match serve_http_once(body) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let source = match signal_source_for_http_url(url.as_str()) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let settings = match pre_004_settings() {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let signal = match pre_004_signal(&source, super::RawTransactionIngestSourceFamily::Transaction, 42, 7, &["filter-a", "filter-b"], 0) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let received_at = match ksp_store_lib::RawTimestamp::from_unix_millis(1_760_000_124_000) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let outcome = match super::hydrate_yellowstone_signal(&source, &settings, signal, received_at).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let _request = match server.join() {
|
|
std::result::Result::Ok(std::result::Result::Ok(value)) => value,
|
|
_ => return,
|
|
};
|
|
let ingress = match outcome {
|
|
super::RawTransactionIngestHydrationOutcome::Available(value) => value,
|
|
super::RawTransactionIngestHydrationOutcome::Missing(_) => return,
|
|
};
|
|
let filter_id = match ingress.provenance.filter_id() {
|
|
std::option::Option::Some(value) => value.as_str(),
|
|
std::option::Option::None => return,
|
|
};
|
|
assert!(filter_id.starts_with("sha256."));
|
|
assert_eq!(filter_id.len(), 71);
|
|
let (mut admission, sender) = crate::RawTransactionAdmission::new(1);
|
|
if sender.send(*ingress).await.is_err() {
|
|
return;
|
|
}
|
|
std::mem::drop(sender);
|
|
let acquisition = match admission.receive(settings.network()).await {
|
|
std::result::Result::Ok(std::option::Option::Some(value)) => value,
|
|
_ => return,
|
|
};
|
|
assert_eq!(acquisition.transaction().block_time(), std::option::Option::None);
|
|
let payload = match std::str::from_utf8(acquisition.transaction().payload().bytes()) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
assert!(payload.contains("\"meta\":null"));
|
|
assert!(payload.contains("\"version\":0"));
|
|
assert!(!payload.contains("transactionIndex"));
|
|
return;
|
|
}
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn pre_004_source_hydration_rejects_slot_index_signature_and_network_mismatch_before_admission() {
|
|
for (slot, index, transaction_data, expected_condition) in [
|
|
(43_u64, 7_u32, PRE_004_ZERO_TRANSACTION_BASE64, "hydration.slot_mismatch"),
|
|
(42_u64, 8_u32, PRE_004_ZERO_TRANSACTION_BASE64, "hydration.transaction_index_mismatch"),
|
|
(42_u64, 7_u32, PRE_004_OTHER_TRANSACTION_BASE64, "hydration.signature_mismatch"),
|
|
] {
|
|
let body = std::format!(
|
|
"{{\"jsonrpc\":\"2.0\",\"result\":{{\"slot\":{},\"blockTime\":1760000120,\"transaction\":[\"{}\",\"base64\"],\"meta\":null,\"version\":\"legacy\",\"transactionIndex\":{}}},\"id\":1}}",
|
|
slot,
|
|
transaction_data,
|
|
index,
|
|
);
|
|
let (url, server) = match serve_http_once(body) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let source = match signal_source_for_http_url(url.as_str()) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let settings = match pre_004_settings() {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let signal = match pre_004_signal(&source, super::RawTransactionIngestSourceFamily::Transaction, 42, 7, &["tx-fixture"], 0) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let received_at = match ksp_store_lib::RawTimestamp::from_unix_millis(1_760_000_124_000) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let error = match super::hydrate_yellowstone_signal(&source, &settings, signal, received_at).await {
|
|
std::result::Result::Ok(_) => return,
|
|
std::result::Result::Err(error) => error,
|
|
};
|
|
let _request = match server.join() {
|
|
std::result::Result::Ok(std::result::Result::Ok(value)) => value,
|
|
_ => return,
|
|
};
|
|
assert_eq!(error.code(), crate::ERROR_CODE_RAW_TRANSACTION_INGEST_RUNTIME_INVALID);
|
|
assert!(error.context().iter().any(|context| return context.value() == expected_condition));
|
|
}
|
|
let source = match signal_source() {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let settings = match pre_004_settings() {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let mut signal = match pre_004_signal(&source, super::RawTransactionIngestSourceFamily::Transaction, 42, 7, &["tx-fixture"], 0) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
signal.network = match ksp_store_lib::RawNetworkId::new("mainnet") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let received_at = match ksp_store_lib::RawTimestamp::from_unix_millis(1_760_000_124_000) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let error = match super::hydrate_yellowstone_signal(&source, &settings, signal, received_at).await {
|
|
std::result::Result::Ok(_) => return,
|
|
std::result::Result::Err(error) => error,
|
|
};
|
|
assert_eq!(error.code(), crate::ERROR_CODE_RAW_TRANSACTION_INGEST_RUNTIME_INVALID);
|
|
assert!(error.context().iter().any(|context| return context.value() == "hydration.network_mismatch"));
|
|
return;
|
|
}
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn pre_004_status_provenance_and_future_source_timestamp_are_bounded_and_redacted() {
|
|
let source = match signal_source() {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let settings = match pre_004_settings() {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let mut signal = match pre_004_signal(&source, super::RawTransactionIngestSourceFamily::TransactionStatus, 42, 7, &["filter-b", "filter-a", "filter-a"], 0)
|
|
{
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
signal.created_at = std::option::Option::Some(super::RawTransactionIngestSourceTimestamp { seconds: 1_760_000_200, nanos: 0 });
|
|
let received_at = match ksp_store_lib::RawTimestamp::from_unix_millis(1_760_000_124_000) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let hydration = source.hydration_context();
|
|
let provenance = match super::build_hydration_provenance(
|
|
&hydration,
|
|
&settings,
|
|
&signal,
|
|
"fixture-http-provider",
|
|
"http-hydration-fixture",
|
|
ksp_onchain_transport_lib::SolanaCommitment::Confirmed,
|
|
received_at,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
assert_eq!(provenance.acquisition_method().as_str(), "status_get_transaction");
|
|
assert_eq!(provenance.observed_at(), std::option::Option::None);
|
|
let filter_id = match provenance.filter_id() {
|
|
std::option::Option::Some(value) => value.as_str(),
|
|
std::option::Option::None => return,
|
|
};
|
|
assert!(filter_id.starts_with("sha256."));
|
|
let debug = std::format!("{provenance:?}");
|
|
assert!(!debug.contains("filter-a"));
|
|
assert!(!debug.contains("filter-b"));
|
|
assert!(!debug.contains(PRE_004_ZERO_SIGNATURE_TEXT));
|
|
return;
|
|
}
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn pre_006_coalescence_key_merges_transaction_and_status_before_http_fanout() {
|
|
let source = match signal_source() {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let settings = match pre_004_settings() {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let transaction = match pre_004_signal(&source, super::RawTransactionIngestSourceFamily::Transaction, 91, 3, &["transaction-filter"], 0x44) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let status = match pre_004_signal(&source, super::RawTransactionIngestSourceFamily::TransactionStatus, 91, 3, &["status-filter"], 0x44) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let hydration = source.hydration_context();
|
|
let transaction_key = match super::hydration_key(&hydration, &transaction) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let status_key = match super::hydration_key(&hydration, &status) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
assert!(transaction_key == status_key);
|
|
let received_at = match ksp_store_lib::RawTimestamp::from_unix_millis(1_760_000_200_000) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let mut coordinator = super::RawTransactionIngestHydrationCoordinator::new(&settings);
|
|
let (frontier_sender, _frontier_receiver) = tokio::sync::watch::channel(crate::RawTransactionIngestProcessingFrontierProjection::empty());
|
|
let mut processing_frontier = super::RawTransactionIngestProcessingFrontierReporter::new(frontier_sender);
|
|
if coordinator.queue_signal(&hydration, transaction, received_at, &mut processing_frontier).is_err() {
|
|
return;
|
|
}
|
|
if coordinator.queue_signal(&hydration, status, received_at, &mut processing_frontier).is_err() {
|
|
return;
|
|
}
|
|
assert_eq!(coordinator.pending.len(), 1);
|
|
assert_eq!(coordinator.pending_signal_count, 2);
|
|
assert_eq!(coordinator.tasks.len(), 1);
|
|
coordinator.abort_all(&mut processing_frontier).await;
|
|
assert_eq!(coordinator.pending_signal_count, 0);
|
|
assert!(coordinator.pending.is_empty());
|
|
assert!(coordinator.tasks.is_empty());
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn pre_006_coalescence_key_separates_network_signature_and_commitment() {
|
|
let source = match signal_source() {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let first = match pre_004_signal(&source, super::RawTransactionIngestSourceFamily::Transaction, 12, 1, &["same"], 0x10) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let second = match pre_004_signal(&source, super::RawTransactionIngestSourceFamily::Block, 12, 1, &["same"], 0x11) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let hydration = source.hydration_context();
|
|
let first_key = match super::hydration_key(&hydration, &first) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let second_key = match super::hydration_key(&hydration, &second) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
assert!(first_key != second_key);
|
|
assert_eq!(first_key.commitment, "confirmed");
|
|
assert_eq!(first_key.network.as_str(), "devnet");
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn v0_3_15_pre_004_websocket_route_constructors_require_explicit_capability_before_io() {
|
|
let hydration_pool = match http_pool("devnet", "hydration", "get_transaction") {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let undeclared_logs = match ws_endpoint_with_capabilities(
|
|
"devnet",
|
|
"standard-logs-undeclared",
|
|
"fixture-ws-provider",
|
|
ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard,
|
|
std::option::Option::None,
|
|
) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let undeclared_logs = crate::RawTransactionIngestStandardLogsSource::new(
|
|
undeclared_logs,
|
|
ksp_onchain_transport_lib::SolanaLogsSubscribeFilter::All,
|
|
ksp_onchain_transport_lib::SolanaCommitment::Confirmed,
|
|
hydration_pool.clone(),
|
|
ksp_onchain_transport_lib::HttpRoleName::new("hydration"),
|
|
);
|
|
let undeclared_logs = match undeclared_logs {
|
|
std::result::Result::Ok(_) => return,
|
|
std::result::Result::Err(value) => value,
|
|
};
|
|
assert!(undeclared_logs.context().iter().any(|context| return context.value() == "runtime_resources.standard_logs_capability_missing"));
|
|
let wrong_logs = match ws_endpoint_with_capabilities(
|
|
"devnet",
|
|
"standard-logs-wrong",
|
|
"fixture-ws-provider",
|
|
ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard,
|
|
std::option::Option::Some(std::vec![ksp_onchain_transport_lib::WsSubscriptionKind::Slot]),
|
|
) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let wrong_logs = crate::RawTransactionIngestStandardLogsSource::new(
|
|
wrong_logs,
|
|
ksp_onchain_transport_lib::SolanaLogsSubscribeFilter::All,
|
|
ksp_onchain_transport_lib::SolanaCommitment::Confirmed,
|
|
hydration_pool.clone(),
|
|
ksp_onchain_transport_lib::HttpRoleName::new("hydration"),
|
|
);
|
|
let wrong_logs = match wrong_logs {
|
|
std::result::Result::Ok(_) => return,
|
|
std::result::Result::Err(value) => value,
|
|
};
|
|
assert!(wrong_logs.context().iter().any(|context| return context.value() == "runtime_resources.standard_logs_capability_missing"));
|
|
let logs = match ws_endpoint_with_capabilities(
|
|
"devnet",
|
|
"standard-logs-exact",
|
|
"fixture-ws-provider",
|
|
ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard,
|
|
std::option::Option::Some(std::vec![ksp_onchain_transport_lib::WsSubscriptionKind::Logs]),
|
|
) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
assert!(
|
|
crate::RawTransactionIngestStandardLogsSource::new(
|
|
logs,
|
|
ksp_onchain_transport_lib::SolanaLogsSubscribeFilter::All,
|
|
ksp_onchain_transport_lib::SolanaCommitment::Confirmed,
|
|
hydration_pool.clone(),
|
|
ksp_onchain_transport_lib::HttpRoleName::new("hydration"),
|
|
)
|
|
.is_ok()
|
|
);
|
|
let undeclared_block = match ws_endpoint_with_capabilities(
|
|
"devnet",
|
|
"standard-block-undeclared",
|
|
"fixture-ws-provider",
|
|
ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard,
|
|
std::option::Option::None,
|
|
) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let undeclared_block = crate::RawTransactionIngestStandardBlockSource::new(
|
|
undeclared_block,
|
|
ksp_onchain_transport_lib::SolanaBlockSubscribeFilter::All,
|
|
ksp_onchain_transport_lib::SolanaCommitment::Confirmed,
|
|
);
|
|
let undeclared_block = match undeclared_block {
|
|
std::result::Result::Ok(_) => return,
|
|
std::result::Result::Err(value) => value,
|
|
};
|
|
assert!(undeclared_block.context().iter().any(|context| return context.value() == "runtime_resources.standard_block_capability_missing"));
|
|
let block = match ws_endpoint_with_capabilities(
|
|
"devnet",
|
|
"standard-block-exact",
|
|
"fixture-ws-provider",
|
|
ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard,
|
|
std::option::Option::Some(std::vec![ksp_onchain_transport_lib::WsSubscriptionKind::Block]),
|
|
) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
assert!(
|
|
crate::RawTransactionIngestStandardBlockSource::new(
|
|
block,
|
|
ksp_onchain_transport_lib::SolanaBlockSubscribeFilter::All,
|
|
ksp_onchain_transport_lib::SolanaCommitment::Confirmed,
|
|
)
|
|
.is_ok()
|
|
);
|
|
let missing_helius = match ws_endpoint_with_capabilities(
|
|
"devnet",
|
|
"helius-transaction-missing",
|
|
"fixture-helius-provider",
|
|
ksp_onchain_transport_lib::WsProtocolKind::HeliusLaserStream,
|
|
std::option::Option::Some(std::vec![ksp_onchain_transport_lib::WsSubscriptionKind::Logs]),
|
|
) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let missing_helius = crate::RawTransactionIngestHeliusTransactionSource::new(
|
|
missing_helius,
|
|
helius_transaction_filter(false),
|
|
ksp_onchain_transport_lib::SolanaCommitment::Confirmed,
|
|
hydration_pool.clone(),
|
|
ksp_onchain_transport_lib::HttpRoleName::new("hydration"),
|
|
);
|
|
let missing_helius = match missing_helius {
|
|
std::result::Result::Ok(_) => return,
|
|
std::result::Result::Err(value) => value,
|
|
};
|
|
assert!(missing_helius.context().iter().any(|context| return context.value() == "runtime_resources.helius_transaction_capability_missing"));
|
|
let helius = match ws_endpoint_with_capabilities(
|
|
"devnet",
|
|
"helius-transaction-exact",
|
|
"fixture-helius-provider",
|
|
ksp_onchain_transport_lib::WsProtocolKind::HeliusLaserStream,
|
|
std::option::Option::Some(std::vec![ksp_onchain_transport_lib::WsSubscriptionKind::HeliusTransaction]),
|
|
) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
assert!(
|
|
crate::RawTransactionIngestHeliusTransactionSource::new(
|
|
helius,
|
|
helius_transaction_filter(false),
|
|
ksp_onchain_transport_lib::SolanaCommitment::Confirmed,
|
|
hydration_pool,
|
|
ksp_onchain_transport_lib::HttpRoleName::new("hydration"),
|
|
)
|
|
.is_ok()
|
|
);
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn v0_3_13_pre_003_standard_logs_source_validates_protocol_commitment_and_hydration_route() {
|
|
let accepted = standard_logs_source(
|
|
"devnet",
|
|
"standard-logs-a",
|
|
ksp_onchain_transport_lib::SolanaLogsSubscribeFilter::All,
|
|
ksp_onchain_transport_lib::SolanaCommitment::Confirmed,
|
|
"hydration",
|
|
);
|
|
let accepted = match accepted {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let resources = crate::RawTransactionIngestRuntimeResources::from_standard_logs_source(accepted);
|
|
assert_eq!(resources.source_count(), 1);
|
|
let helius_endpoint =
|
|
match ws_endpoint("devnet", "standard-logs-helius", "fixture-ws-provider", ksp_onchain_transport_lib::WsProtocolKind::HeliusLaserStream) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let pool = match http_pool("devnet", "hydration", "get_transaction") {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let protocol_error = crate::RawTransactionIngestStandardLogsSource::new(
|
|
helius_endpoint,
|
|
ksp_onchain_transport_lib::SolanaLogsSubscribeFilter::All,
|
|
ksp_onchain_transport_lib::SolanaCommitment::Confirmed,
|
|
pool.clone(),
|
|
ksp_onchain_transport_lib::HttpRoleName::new("hydration"),
|
|
);
|
|
let protocol_error = match protocol_error {
|
|
std::result::Result::Ok(_) => return,
|
|
std::result::Result::Err(value) => value,
|
|
};
|
|
assert!(protocol_error.context().iter().any(|context| return context.value() == "runtime_resources.standard_logs_protocol_invalid"));
|
|
let standard_endpoint =
|
|
match ws_endpoint("devnet", "standard-logs-processed", "fixture-ws-provider", ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let commitment_error = crate::RawTransactionIngestStandardLogsSource::new(
|
|
standard_endpoint,
|
|
ksp_onchain_transport_lib::SolanaLogsSubscribeFilter::AllWithVotes,
|
|
ksp_onchain_transport_lib::SolanaCommitment::Processed,
|
|
pool,
|
|
ksp_onchain_transport_lib::HttpRoleName::new("hydration"),
|
|
);
|
|
let commitment_error = match commitment_error {
|
|
std::result::Result::Ok(_) => return,
|
|
std::result::Result::Err(value) => value,
|
|
};
|
|
assert!(commitment_error.context().iter().any(|context| return context.value() == "runtime_resources.hydration_commitment_invalid"));
|
|
let cross_network_endpoint =
|
|
match ws_endpoint("devnet", "standard-logs-cross-network", "fixture-ws-provider", ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let cross_network_pool = match http_pool("testnet", "hydration", "get_transaction") {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let cross_network = crate::RawTransactionIngestStandardLogsSource::new(
|
|
cross_network_endpoint,
|
|
ksp_onchain_transport_lib::SolanaLogsSubscribeFilter::All,
|
|
ksp_onchain_transport_lib::SolanaCommitment::Confirmed,
|
|
cross_network_pool,
|
|
ksp_onchain_transport_lib::HttpRoleName::new("hydration"),
|
|
);
|
|
let cross_network = match cross_network {
|
|
std::result::Result::Ok(_) => return,
|
|
std::result::Result::Err(value) => value,
|
|
};
|
|
assert!(cross_network.context().iter().any(|context| return context.value() == "runtime_resources.transport_network_mismatch"));
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn v0_3_13_pre_003_standard_logs_source_key_is_filter_sensitive_hydration_role_neutral_and_redacted() {
|
|
let all_a = match standard_logs_source(
|
|
"devnet",
|
|
"standard-logs-key",
|
|
ksp_onchain_transport_lib::SolanaLogsSubscribeFilter::All,
|
|
ksp_onchain_transport_lib::SolanaCommitment::Confirmed,
|
|
"hydration",
|
|
) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let all_b = match standard_logs_source(
|
|
"devnet",
|
|
"standard-logs-key",
|
|
ksp_onchain_transport_lib::SolanaLogsSubscribeFilter::All,
|
|
ksp_onchain_transport_lib::SolanaCommitment::Confirmed,
|
|
"secondary_hydration",
|
|
) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let votes = match standard_logs_source(
|
|
"devnet",
|
|
"standard-logs-key",
|
|
ksp_onchain_transport_lib::SolanaLogsSubscribeFilter::AllWithVotes,
|
|
ksp_onchain_transport_lib::SolanaCommitment::Confirmed,
|
|
"hydration",
|
|
) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
assert_eq!(all_a.source_key, all_b.source_key);
|
|
assert_ne!(all_a.source_key, votes.source_key);
|
|
let mention = ksp_core_lib::Pubkey::new_from_array([0x5a_u8; 32]);
|
|
let mention_text = mention.to_string();
|
|
let mentioned = match standard_logs_source(
|
|
"devnet",
|
|
"standard-logs-key",
|
|
ksp_onchain_transport_lib::SolanaLogsSubscribeFilter::Mentions(mention),
|
|
ksp_onchain_transport_lib::SolanaCommitment::Confirmed,
|
|
"hydration",
|
|
) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
assert_ne!(all_a.source_key, mentioned.source_key);
|
|
let debug = std::format!("{mentioned:?}");
|
|
assert!(debug.contains("filter_kind"));
|
|
assert!(debug.contains("mentions"));
|
|
assert!(!debug.contains(mention_text.as_str()));
|
|
assert!(!debug.contains("WS-SECRET-CANARY"));
|
|
assert!(!debug.contains("source_key: ["));
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn v0_3_13_pre_003_standard_logs_projection_keeps_only_reference_material() {
|
|
let source = match standard_logs_source(
|
|
"devnet",
|
|
"standard-logs-projection",
|
|
ksp_onchain_transport_lib::SolanaLogsSubscribeFilter::All,
|
|
ksp_onchain_transport_lib::SolanaCommitment::Finalized,
|
|
"hydration",
|
|
) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let fixture = StandardLogsViewFixture { signature: PRE_004_ZERO_SIGNATURE_TEXT.to_owned(), slot: 73 };
|
|
let signal = match super::project_standard_logs_signal(&source, &fixture) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
assert_eq!(signal.family, super::RawTransactionIngestSourceFamily::Logs);
|
|
assert_eq!(signal.slot, 73);
|
|
assert_eq!(signal.transaction_index, std::option::Option::None);
|
|
assert_eq!(signal.created_at, std::option::Option::None);
|
|
assert_eq!(signal.matched_filter_count, 1);
|
|
assert_eq!(signal.matched_filter_id, std::option::Option::None);
|
|
assert_eq!(signal.signature, ksp_store_lib::RawTransactionSignature::new([0_u8; 64]));
|
|
let debug = std::format!("{signal:?}");
|
|
assert!(!debug.contains(PRE_004_ZERO_SIGNATURE_TEXT));
|
|
return;
|
|
}
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn v0_3_13_pre_003_standard_logs_reference_hydrates_once_to_exact_common_raw() {
|
|
let body = std::format!(
|
|
"{{\"jsonrpc\":\"2.0\",\"result\":{{\"slot\":42,\"blockTime\":1760000120,\"transaction\":[\"{}\",\"base64\"],\"meta\":{{\"err\":null,\"fee\":5000}},\"version\":\"legacy\",\"transactionIndex\":7}},\"id\":1}}",
|
|
PRE_004_ZERO_TRANSACTION_BASE64,
|
|
);
|
|
let (url, server) = match serve_http_once(body) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let source = match standard_logs_source_for_http_url(url.as_str(), ksp_onchain_transport_lib::SolanaLogsSubscribeFilter::All) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let settings = match pre_004_settings() {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let fixture = StandardLogsViewFixture { signature: PRE_004_ZERO_SIGNATURE_TEXT.to_owned(), slot: 42 };
|
|
let signal = match super::project_standard_logs_signal(&source, &fixture) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let received_at = match ksp_store_lib::RawTimestamp::from_unix_millis(1_760_000_200_000) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let hydration = source.hydration_context();
|
|
let key = match super::hydration_key(&hydration, &signal) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let fetched =
|
|
super::fetch_hydration(hydration.http_pool.clone(), hydration.hydration_role.clone(), hydration.network.clone(), key, hydration.commitment).await;
|
|
let fetched = match fetched {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let ingress = super::finalize_hydration(&hydration, &settings, signal, received_at, &fetched.observed);
|
|
let ingress = match ingress {
|
|
std::result::Result::Ok(std::option::Option::Some(value)) => value,
|
|
std::result::Result::Ok(std::option::Option::None) | std::result::Result::Err(_) => return,
|
|
};
|
|
assert_eq!(ingress.provenance.protocol().as_str(), "solana_ws_http");
|
|
assert_eq!(ingress.provenance.acquisition_method().as_str(), "logs_get_transaction");
|
|
let (mut admission, sender) = crate::RawTransactionAdmission::new(1);
|
|
if sender.send(ingress).await.is_err() {
|
|
return;
|
|
}
|
|
std::mem::drop(sender);
|
|
let acquisition = match admission.receive(settings.network()).await {
|
|
std::result::Result::Ok(std::option::Option::Some(value)) => value,
|
|
_ => return,
|
|
};
|
|
assert_eq!(acquisition.transaction().reference().network().as_str(), "devnet");
|
|
assert_eq!(acquisition.transaction().reference().signature().as_bytes(), &[0_u8; 64]);
|
|
assert_eq!(acquisition.transaction().slot(), 42);
|
|
assert_eq!(acquisition.transaction().block_time().map(|value| return value.unix_millis()), std::option::Option::Some(1_760_000_120_000));
|
|
assert_eq!(acquisition.observation().provenance().protocol().as_str(), "solana_ws_http");
|
|
assert_eq!(acquisition.observation().provenance().acquisition_method().as_str(), "logs_get_transaction");
|
|
let request_body = match server.join() {
|
|
std::result::Result::Ok(std::result::Result::Ok(value)) => value,
|
|
std::result::Result::Ok(std::result::Result::Err(_)) | std::result::Result::Err(_) => return,
|
|
};
|
|
assert!(request_body.contains("\"method\":\"getTransaction\""));
|
|
assert!(request_body.contains("\"encoding\":\"base64\""));
|
|
assert!(request_body.contains("\"maxSupportedTransactionVersion\":1"));
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn v0_3_13_pre_003_runtime_resources_reject_duplicate_standard_logs_identity() {
|
|
let first = match standard_logs_source(
|
|
"devnet",
|
|
"standard-logs-duplicate",
|
|
ksp_onchain_transport_lib::SolanaLogsSubscribeFilter::AllWithVotes,
|
|
ksp_onchain_transport_lib::SolanaCommitment::Confirmed,
|
|
"hydration",
|
|
) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let duplicate = match standard_logs_source(
|
|
"devnet",
|
|
"standard-logs-duplicate",
|
|
ksp_onchain_transport_lib::SolanaLogsSubscribeFilter::AllWithVotes,
|
|
ksp_onchain_transport_lib::SolanaCommitment::Confirmed,
|
|
"secondary_hydration",
|
|
) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let mut resources = crate::RawTransactionIngestRuntimeResources::from_standard_logs_source(first);
|
|
let result = resources.try_push_standard_logs_source(duplicate);
|
|
let error = match result {
|
|
std::result::Result::Ok(()) => return,
|
|
std::result::Result::Err(value) => value,
|
|
};
|
|
assert!(error.context().iter().any(|context| return context.value() == "runtime_resources.duplicate_source_identity"));
|
|
assert_eq!(resources.source_count(), 1);
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn v0_3_13_pre_004_standard_block_source_validates_protocol_commitment_and_identity() {
|
|
let source = match standard_block_source(
|
|
"devnet",
|
|
"standard-block-a",
|
|
ksp_onchain_transport_lib::SolanaBlockSubscribeFilter::All,
|
|
ksp_onchain_transport_lib::SolanaCommitment::Confirmed,
|
|
) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let same = match standard_block_source(
|
|
"devnet",
|
|
"standard-block-a",
|
|
ksp_onchain_transport_lib::SolanaBlockSubscribeFilter::All,
|
|
ksp_onchain_transport_lib::SolanaCommitment::Confirmed,
|
|
) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let pubkey = ksp_core_lib::Pubkey::new_from_array([7_u8; 32]);
|
|
let filtered = match standard_block_source(
|
|
"devnet",
|
|
"standard-block-a",
|
|
ksp_onchain_transport_lib::SolanaBlockSubscribeFilter::MentionsAccountOrProgram(pubkey),
|
|
ksp_onchain_transport_lib::SolanaCommitment::Confirmed,
|
|
) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
assert_eq!(source.source_key, same.source_key);
|
|
assert_ne!(source.source_key, filtered.source_key);
|
|
let debug = std::format!("{filtered:?}");
|
|
assert!(debug.contains("mentions_account_or_program"));
|
|
assert!(!debug.contains("WS-SECRET-CANARY"));
|
|
assert!(!debug.contains(&pubkey.to_string()));
|
|
let invalid_endpoint = match ws_endpoint("devnet", "invalid-block", "fixture-ws-provider", ksp_onchain_transport_lib::WsProtocolKind::HeliusLaserStream) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
assert!(
|
|
crate::RawTransactionIngestStandardBlockSource::new(
|
|
invalid_endpoint,
|
|
ksp_onchain_transport_lib::SolanaBlockSubscribeFilter::All,
|
|
ksp_onchain_transport_lib::SolanaCommitment::Confirmed,
|
|
)
|
|
.is_err()
|
|
);
|
|
let processed_endpoint = match ws_endpoint("devnet", "processed-block", "fixture-ws-provider", ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
assert!(
|
|
crate::RawTransactionIngestStandardBlockSource::new(
|
|
processed_endpoint,
|
|
ksp_onchain_transport_lib::SolanaBlockSubscribeFilter::All,
|
|
ksp_onchain_transport_lib::SolanaCommitment::Processed,
|
|
)
|
|
.is_err()
|
|
);
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn v0_3_13_pre_004_standard_block_qualifies_legacy_v0_v1_and_rejects_ambiguous_or_future_versions() {
|
|
for version in [
|
|
ksp_onchain_transport_lib::SolanaTransactionVersion::Legacy,
|
|
ksp_onchain_transport_lib::SolanaTransactionVersion::Number(0),
|
|
ksp_onchain_transport_lib::SolanaTransactionVersion::Number(1),
|
|
] {
|
|
let qualified = super::qualify_standard_block_version(&ksp_onchain_transport_lib::SolanaWireField::Value(version));
|
|
assert!(qualified.is_ok());
|
|
}
|
|
assert!(
|
|
super::qualify_standard_block_version(&ksp_onchain_transport_lib::SolanaWireField::Value(ksp_onchain_transport_lib::SolanaTransactionVersion::Number(
|
|
2
|
|
),))
|
|
.is_err()
|
|
);
|
|
assert!(super::qualify_standard_block_version(&ksp_onchain_transport_lib::SolanaWireField::Omitted).is_err());
|
|
assert!(super::qualify_standard_block_version(&ksp_onchain_transport_lib::SolanaWireField::Null).is_err());
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn v0_3_13_pre_004_legacy_v0_v1_block_materials_are_direct_common_raw_with_exact_version_and_index() {
|
|
let legacy_message = ksp_raw_transaction_lib::RawSolanaTransactionMessage::new(
|
|
ksp_raw_transaction_lib::RawSolanaMessageVersion::Legacy,
|
|
ksp_raw_transaction_lib::RawSolanaMessageHeader::new(1, 0, 1),
|
|
vec![[0x21_u8; 32], [0x22_u8; 32]],
|
|
[0x31_u8; 32],
|
|
vec![ksp_raw_transaction_lib::RawSolanaCompiledInstruction::new(1, vec![0], vec![0xaa, 0xbb])],
|
|
vec![],
|
|
std::option::Option::None,
|
|
);
|
|
let v0_message = ksp_raw_transaction_lib::RawSolanaTransactionMessage::new(
|
|
ksp_raw_transaction_lib::RawSolanaMessageVersion::V0,
|
|
ksp_raw_transaction_lib::RawSolanaMessageHeader::new(1, 0, 0),
|
|
vec![[0x23_u8; 32]],
|
|
[0x32_u8; 32],
|
|
vec![ksp_raw_transaction_lib::RawSolanaCompiledInstruction::new(0, vec![1], vec![0xcc])],
|
|
vec![ksp_raw_transaction_lib::RawSolanaAddressTableLookup::new([0x24_u8; 32], vec![0], vec![])],
|
|
std::option::Option::None,
|
|
);
|
|
let v1_message = ksp_raw_transaction_lib::RawSolanaTransactionMessage::new(
|
|
ksp_raw_transaction_lib::RawSolanaMessageVersion::V1,
|
|
ksp_raw_transaction_lib::RawSolanaMessageHeader::new(1, 0, 1),
|
|
vec![[0x41_u8; 32], [0x42_u8; 32]],
|
|
[0x51_u8; 32],
|
|
vec![ksp_raw_transaction_lib::RawSolanaCompiledInstruction::new(1, vec![0], vec![1, 2, 3])],
|
|
vec![],
|
|
std::option::Option::Some(ksp_raw_transaction_lib::RawSolanaTransactionConfig::new(
|
|
std::option::Option::Some(7),
|
|
std::option::Option::Some(8),
|
|
std::option::Option::Some(9),
|
|
std::option::Option::Some(32_768),
|
|
)),
|
|
);
|
|
let fixtures = [
|
|
(
|
|
ksp_raw_transaction_lib::RawSolanaTransactionWire::new(vec![[0x11_u8; 64]], legacy_message),
|
|
ksp_onchain_transport_lib::SolanaTransactionVersion::Legacy,
|
|
[0x11_u8; 64],
|
|
"\"version\":\"legacy\"",
|
|
1_usize,
|
|
),
|
|
(
|
|
ksp_raw_transaction_lib::RawSolanaTransactionWire::new(vec![[0x12_u8; 64]], v0_message),
|
|
ksp_onchain_transport_lib::SolanaTransactionVersion::Number(0),
|
|
[0x12_u8; 64],
|
|
"\"version\":0",
|
|
2_usize,
|
|
),
|
|
(
|
|
ksp_raw_transaction_lib::RawSolanaTransactionWire::new(vec![[0xaa_u8; 64]], v1_message),
|
|
ksp_onchain_transport_lib::SolanaTransactionVersion::Number(1),
|
|
[0xaa_u8; 64],
|
|
"\"version\":1",
|
|
3_usize,
|
|
),
|
|
];
|
|
let network = match ksp_store_lib::RawNetworkId::new("devnet") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
for (wire, version, expected_signature, expected_version, position) in fixtures {
|
|
let encoded = match ksp_raw_transaction_lib::serialize_solana_transaction_wire_base64(&wire) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let material = super::build_standard_block_material(
|
|
&ksp_onchain_transport_lib::SolanaWireField::Value(version),
|
|
position,
|
|
|qualified_version, transaction_index| {
|
|
return ksp_raw_transaction_lib::RawTransactionMaterial::binary_base64_with_embedded_signature(
|
|
network.clone(),
|
|
430_000_123,
|
|
std::option::Option::Some(1_787_072_400),
|
|
encoded.as_str(),
|
|
ksp_raw_transaction_lib::RawTransactionWireField::Null,
|
|
qualified_version,
|
|
transaction_index,
|
|
);
|
|
},
|
|
);
|
|
let material = match material {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let raw = match ksp_raw_transaction_lib::canonicalize_raw_transaction(material) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
assert_eq!(raw.reference().signature().as_bytes(), &expected_signature);
|
|
assert_eq!(raw.slot(), 430_000_123);
|
|
assert_eq!(raw.block_time().map(|value| return value.unix_millis()), std::option::Option::Some(1_787_072_400_000));
|
|
let payload = match std::str::from_utf8(raw.payload().bytes()) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
assert!(payload.contains(expected_version));
|
|
assert!(payload.contains(&std::format!("\"transactionIndex\":{position}")));
|
|
assert!(payload.contains(encoded.as_str()));
|
|
}
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn v0_3_13_pre_004_runtime_resources_reject_duplicate_standard_block_identity() {
|
|
let first = match standard_block_source(
|
|
"devnet",
|
|
"standard-block-duplicate",
|
|
ksp_onchain_transport_lib::SolanaBlockSubscribeFilter::All,
|
|
ksp_onchain_transport_lib::SolanaCommitment::Finalized,
|
|
) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let duplicate = match standard_block_source(
|
|
"devnet",
|
|
"standard-block-duplicate",
|
|
ksp_onchain_transport_lib::SolanaBlockSubscribeFilter::All,
|
|
ksp_onchain_transport_lib::SolanaCommitment::Finalized,
|
|
) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let mut resources = crate::RawTransactionIngestRuntimeResources::from_standard_block_source(first);
|
|
let result = resources.try_push_standard_block_source(duplicate);
|
|
let error = match result {
|
|
std::result::Result::Ok(()) => return,
|
|
std::result::Result::Err(value) => value,
|
|
};
|
|
assert!(error.context().iter().any(|context| return context.value() == "runtime_resources.duplicate_source_identity"));
|
|
assert_eq!(resources.source_count(), 1);
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn v0_3_13_pre_005_helius_transaction_source_validates_protocol_commitment_request_and_hydration_route() {
|
|
let source = match helius_transaction_source(
|
|
"devnet",
|
|
"helius-transaction-a",
|
|
helius_transaction_filter(false),
|
|
ksp_onchain_transport_lib::SolanaCommitment::Confirmed,
|
|
"hydration",
|
|
) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let request = super::helius_transaction_request(source.filter.clone(), source.commitment);
|
|
let options = match request.options() {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
assert_eq!(options.commitment(), std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed));
|
|
assert_eq!(options.encoding(), std::option::Option::Some(ksp_onchain_transport_lib::HeliusTransactionSubscribeEncoding::Base64));
|
|
assert_eq!(options.transaction_details(), std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionDetails::Full));
|
|
assert_eq!(options.show_rewards(), std::option::Option::Some(false));
|
|
assert_eq!(options.max_supported_transaction_version(), std::option::Option::Some(1));
|
|
assert!(request.validate().is_ok());
|
|
let resources = crate::RawTransactionIngestRuntimeResources::from_helius_transaction_source(source);
|
|
assert_eq!(resources.source_count(), 1);
|
|
let standard_endpoint =
|
|
match ws_endpoint("devnet", "helius-transaction-standard", "fixture-helius-provider", ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let pool = match http_pool("devnet", "hydration", "get_transaction") {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let protocol_error = crate::RawTransactionIngestHeliusTransactionSource::new(
|
|
standard_endpoint,
|
|
helius_transaction_filter(false),
|
|
ksp_onchain_transport_lib::SolanaCommitment::Confirmed,
|
|
pool.clone(),
|
|
ksp_onchain_transport_lib::HttpRoleName::new("hydration"),
|
|
);
|
|
let protocol_error = match protocol_error {
|
|
std::result::Result::Ok(_) => return,
|
|
std::result::Result::Err(value) => value,
|
|
};
|
|
assert!(protocol_error.context().iter().any(|context| return context.value() == "runtime_resources.helius_transaction_protocol_invalid"));
|
|
let helius_endpoint =
|
|
match ws_endpoint("devnet", "helius-transaction-processed", "fixture-helius-provider", ksp_onchain_transport_lib::WsProtocolKind::HeliusLaserStream) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let commitment_error = crate::RawTransactionIngestHeliusTransactionSource::new(
|
|
helius_endpoint,
|
|
helius_transaction_filter(false),
|
|
ksp_onchain_transport_lib::SolanaCommitment::Processed,
|
|
pool,
|
|
ksp_onchain_transport_lib::HttpRoleName::new("hydration"),
|
|
);
|
|
let commitment_error = match commitment_error {
|
|
std::result::Result::Ok(_) => return,
|
|
std::result::Result::Err(value) => value,
|
|
};
|
|
assert!(commitment_error.context().iter().any(|context| return context.value() == "runtime_resources.hydration_commitment_invalid"));
|
|
let cross_network_endpoint = match ws_endpoint(
|
|
"devnet",
|
|
"helius-transaction-cross-network",
|
|
"fixture-helius-provider",
|
|
ksp_onchain_transport_lib::WsProtocolKind::HeliusLaserStream,
|
|
) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let cross_network_pool = match http_pool("testnet", "hydration", "get_transaction") {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let cross_network = crate::RawTransactionIngestHeliusTransactionSource::new(
|
|
cross_network_endpoint,
|
|
helius_transaction_filter(false),
|
|
ksp_onchain_transport_lib::SolanaCommitment::Confirmed,
|
|
cross_network_pool,
|
|
ksp_onchain_transport_lib::HttpRoleName::new("hydration"),
|
|
);
|
|
let cross_network = match cross_network {
|
|
std::result::Result::Ok(_) => return,
|
|
std::result::Result::Err(value) => value,
|
|
};
|
|
assert!(cross_network.context().iter().any(|context| return context.value() == "runtime_resources.transport_network_mismatch"));
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn v0_3_13_pre_005_helius_transaction_source_key_is_filter_sensitive_order_normalized_hydration_role_neutral_and_redacted() {
|
|
let first_pubkey = ksp_core_lib::Pubkey::new_from_array([0x31_u8; 32]);
|
|
let second_pubkey = ksp_core_lib::Pubkey::new_from_array([0x32_u8; 32]);
|
|
let signature_canary = "5HueCGU8rMjxEXxiPuD5BDuRaH7RkZBzQ2hL5M5WnQp9";
|
|
let first_filter = ksp_onchain_transport_lib::HeliusTransactionSubscribeFilter::new(
|
|
std::option::Option::Some(false),
|
|
std::option::Option::Some(false),
|
|
std::option::Option::Some(signature_canary.to_owned()),
|
|
std::option::Option::Some(std::vec![first_pubkey, second_pubkey]),
|
|
std::option::Option::None,
|
|
std::option::Option::None,
|
|
std::option::Option::Some(ksp_onchain_transport_lib::HeliusTokenAccountsFilter::BalanceChanged),
|
|
);
|
|
let reordered_filter = ksp_onchain_transport_lib::HeliusTransactionSubscribeFilter::new(
|
|
std::option::Option::Some(false),
|
|
std::option::Option::Some(false),
|
|
std::option::Option::Some(signature_canary.to_owned()),
|
|
std::option::Option::Some(std::vec![second_pubkey, first_pubkey]),
|
|
std::option::Option::None,
|
|
std::option::Option::None,
|
|
std::option::Option::Some(ksp_onchain_transport_lib::HeliusTokenAccountsFilter::BalanceChanged),
|
|
);
|
|
let changed_filter = ksp_onchain_transport_lib::HeliusTransactionSubscribeFilter::new(
|
|
std::option::Option::Some(false),
|
|
std::option::Option::Some(true),
|
|
std::option::Option::Some(signature_canary.to_owned()),
|
|
std::option::Option::Some(std::vec![first_pubkey, second_pubkey]),
|
|
std::option::Option::None,
|
|
std::option::Option::None,
|
|
std::option::Option::Some(ksp_onchain_transport_lib::HeliusTokenAccountsFilter::BalanceChanged),
|
|
);
|
|
let first = match helius_transaction_source(
|
|
"devnet",
|
|
"helius-transaction-key",
|
|
first_filter,
|
|
ksp_onchain_transport_lib::SolanaCommitment::Confirmed,
|
|
"hydration",
|
|
) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let reordered = match helius_transaction_source(
|
|
"devnet",
|
|
"helius-transaction-key",
|
|
reordered_filter,
|
|
ksp_onchain_transport_lib::SolanaCommitment::Confirmed,
|
|
"secondary_hydration",
|
|
) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let changed = match helius_transaction_source(
|
|
"devnet",
|
|
"helius-transaction-key",
|
|
changed_filter,
|
|
ksp_onchain_transport_lib::SolanaCommitment::Confirmed,
|
|
"hydration",
|
|
) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
assert_eq!(first.source_key, reordered.source_key);
|
|
assert_ne!(first.source_key, changed.source_key);
|
|
let first_pubkey_text = first_pubkey.to_string();
|
|
let debug = std::format!("{first:?}");
|
|
assert!(debug.contains("RawTransactionIngestHeliusTransactionSource"));
|
|
assert!(debug.contains("signature_present"));
|
|
assert!(!debug.contains(signature_canary));
|
|
assert!(!debug.contains(first_pubkey_text.as_str()));
|
|
assert!(!debug.contains("WS-SECRET-CANARY"));
|
|
assert!(!debug.contains("api-key"));
|
|
assert!(!debug.contains("source_key: ["));
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn v0_3_13_pre_005_helius_full_projection_keeps_only_reference_material() {
|
|
let source = match helius_transaction_source(
|
|
"devnet",
|
|
"helius-transaction-projection",
|
|
helius_transaction_filter(false),
|
|
ksp_onchain_transport_lib::SolanaCommitment::Finalized,
|
|
"hydration",
|
|
) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let fixture = HeliusTransactionViewFixture { signature: PRE_004_ZERO_SIGNATURE_TEXT.to_owned(), slot: 81, transaction_index: 9 };
|
|
let signal = match super::project_helius_transaction_signal(&source, &fixture) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
assert_eq!(signal.family, super::RawTransactionIngestSourceFamily::Transaction);
|
|
assert_eq!(signal.slot, 81);
|
|
assert_eq!(signal.transaction_index, std::option::Option::Some(9));
|
|
assert_eq!(signal.created_at, std::option::Option::None);
|
|
assert_eq!(signal.matched_filter_count, 1);
|
|
assert_eq!(signal.matched_filter_id, std::option::Option::None);
|
|
assert_eq!(signal.signature, ksp_store_lib::RawTransactionSignature::new([0_u8; 64]));
|
|
let debug = std::format!("{signal:?}");
|
|
assert!(!debug.contains(PRE_004_ZERO_SIGNATURE_TEXT));
|
|
return;
|
|
}
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn v0_3_13_pre_005_helius_full_reference_hydrates_once_to_exact_common_raw() {
|
|
let body = std::format!(
|
|
"{{\"jsonrpc\":\"2.0\",\"result\":{{\"slot\":42,\"blockTime\":1760000120,\"transaction\":[\"{}\",\"base64\"],\"meta\":null,\"version\":1,\"transactionIndex\":7}},\"id\":1}}",
|
|
PRE_004_ZERO_TRANSACTION_BASE64,
|
|
);
|
|
let (url, server) = match serve_http_once(body) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let source = match helius_transaction_source_for_http_url(url.as_str()) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let settings = match pre_004_settings() {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let fixture = HeliusTransactionViewFixture { signature: PRE_004_ZERO_SIGNATURE_TEXT.to_owned(), slot: 42, transaction_index: 7 };
|
|
let signal = match super::project_helius_transaction_signal(&source, &fixture) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let received_at = match ksp_store_lib::RawTimestamp::from_unix_millis(1_760_000_200_000) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let hydration = source.hydration_context();
|
|
let key = match super::hydration_key(&hydration, &signal) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let fetched =
|
|
super::fetch_hydration(hydration.http_pool.clone(), hydration.hydration_role.clone(), hydration.network.clone(), key, hydration.commitment).await;
|
|
let fetched = match fetched {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let ingress = super::finalize_hydration(&hydration, &settings, signal, received_at, &fetched.observed);
|
|
let ingress = match ingress {
|
|
std::result::Result::Ok(std::option::Option::Some(value)) => value,
|
|
std::result::Result::Ok(std::option::Option::None) | std::result::Result::Err(_) => return,
|
|
};
|
|
assert_eq!(ingress.provenance.protocol().as_str(), "helius_ws_http");
|
|
assert_eq!(ingress.provenance.acquisition_method().as_str(), "transaction_get_transaction");
|
|
let (mut admission, sender) = crate::RawTransactionAdmission::new(1);
|
|
if sender.send(ingress).await.is_err() {
|
|
return;
|
|
}
|
|
std::mem::drop(sender);
|
|
let acquisition = match admission.receive(settings.network()).await {
|
|
std::result::Result::Ok(std::option::Option::Some(value)) => value,
|
|
_ => return,
|
|
};
|
|
assert_eq!(acquisition.transaction().reference().network().as_str(), "devnet");
|
|
assert_eq!(acquisition.transaction().reference().signature().as_bytes(), &[0_u8; 64]);
|
|
assert_eq!(acquisition.transaction().slot(), 42);
|
|
assert_eq!(acquisition.transaction().block_time().map(|value| return value.unix_millis()), std::option::Option::Some(1_760_000_120_000));
|
|
let payload = match std::str::from_utf8(acquisition.transaction().payload().bytes()) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
assert!(payload.contains("\"version\":1"));
|
|
assert!(payload.contains("\"transactionIndex\":7"));
|
|
let request_body = match server.join() {
|
|
std::result::Result::Ok(std::result::Result::Ok(value)) => value,
|
|
std::result::Result::Ok(std::result::Result::Err(_)) | std::result::Result::Err(_) => return,
|
|
};
|
|
assert!(request_body.contains("\"method\":\"getTransaction\""));
|
|
assert!(request_body.contains("\"encoding\":\"base64\""));
|
|
assert!(request_body.contains("\"maxSupportedTransactionVersion\":1"));
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn v0_3_13_pre_005_runtime_resources_reject_duplicate_helius_transaction_identity() {
|
|
let first = match helius_transaction_source(
|
|
"devnet",
|
|
"helius-transaction-duplicate",
|
|
helius_transaction_filter(false),
|
|
ksp_onchain_transport_lib::SolanaCommitment::Confirmed,
|
|
"hydration",
|
|
) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let duplicate = match helius_transaction_source(
|
|
"devnet",
|
|
"helius-transaction-duplicate",
|
|
helius_transaction_filter(false),
|
|
ksp_onchain_transport_lib::SolanaCommitment::Confirmed,
|
|
"secondary_hydration",
|
|
) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let mut resources = crate::RawTransactionIngestRuntimeResources::from_helius_transaction_source(first);
|
|
let result = resources.try_push_helius_transaction_source(duplicate);
|
|
let error = match result {
|
|
std::result::Result::Ok(()) => return,
|
|
std::result::Result::Err(value) => value,
|
|
};
|
|
assert!(error.context().iter().any(|context| return context.value() == "runtime_resources.duplicate_source_identity"));
|
|
assert_eq!(resources.source_count(), 1);
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn pre_007_processing_frontier_never_advances_through_pending_work() {
|
|
let mut frontier = super::RawTransactionIngestProcessingFrontier::new();
|
|
assert!(frontier.observe_settled(100).is_ok());
|
|
let projection = frontier.projection();
|
|
assert_eq!(projection.hydration_pending(), 0);
|
|
assert_eq!(projection.processing_frontier_slot(), std::option::Option::Some(100));
|
|
assert_eq!(projection.oldest_pending_slot(), std::option::Option::None);
|
|
assert!(frontier.observe_pending(102).is_ok());
|
|
assert!(frontier.observe_settled(105).is_ok());
|
|
assert!(frontier.observe_pending(104).is_ok());
|
|
let projection = frontier.projection();
|
|
assert_eq!(projection.hydration_pending(), 2);
|
|
assert_eq!(projection.processing_frontier_slot(), std::option::Option::Some(100));
|
|
assert_eq!(projection.oldest_pending_slot(), std::option::Option::Some(102));
|
|
assert!(frontier.settle_pending(102).is_ok());
|
|
let projection = frontier.projection();
|
|
assert_eq!(projection.hydration_pending(), 1);
|
|
assert_eq!(projection.processing_frontier_slot(), std::option::Option::Some(102));
|
|
assert_eq!(projection.oldest_pending_slot(), std::option::Option::Some(104));
|
|
assert!(frontier.settle_pending(104).is_ok());
|
|
let projection = frontier.projection();
|
|
assert_eq!(projection.hydration_pending(), 0);
|
|
assert_eq!(projection.processing_frontier_slot(), std::option::Option::Some(105));
|
|
assert_eq!(projection.oldest_pending_slot(), std::option::Option::None);
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn pre_007_processing_frontier_compacts_settled_slots_to_pending_intervals() {
|
|
let mut frontier = super::RawTransactionIngestProcessingFrontier::new();
|
|
assert!(frontier.observe_pending(10).is_ok());
|
|
assert!(frontier.observe_pending(20).is_ok());
|
|
for slot in 0_u64..=1000_u64 {
|
|
if slot == 10 || slot == 20 {
|
|
continue;
|
|
}
|
|
assert!(frontier.observe_settled(slot).is_ok());
|
|
}
|
|
assert!(frontier.slot_state_count() <= 5);
|
|
let projection = frontier.projection();
|
|
assert_eq!(projection.hydration_pending(), 2);
|
|
assert_eq!(projection.oldest_pending_slot(), std::option::Option::Some(10));
|
|
assert_eq!(projection.processing_frontier_slot(), std::option::Option::Some(9));
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn pre_008_reconnect_replay_and_proven_gap_are_distinct_monotone_and_frontier_preserving() {
|
|
let (sender, receiver) = tokio::sync::watch::channel(crate::RawTransactionIngestProcessingFrontierProjection::empty());
|
|
let mut reporter = super::RawTransactionIngestProcessingFrontierReporter::new(sender);
|
|
assert!(reporter.observe_pending(42).is_ok());
|
|
assert!(reporter.observe_source_continuity(crate::RawTransactionIngestSourceState::Reconnecting, 0, 1, 0, 0, 0).is_ok());
|
|
let replaying = receiver.borrow().clone();
|
|
assert_eq!(replaying.hydration_pending(), 1);
|
|
assert_eq!(replaying.oldest_pending_slot(), std::option::Option::Some(42));
|
|
assert_eq!(replaying.source_state(), std::option::Option::Some(crate::RawTransactionIngestSourceState::Reconnecting));
|
|
assert_eq!(replaying.source_reconnect_total(), 0);
|
|
assert_eq!(replaying.source_replay_attempt_total(), 1);
|
|
assert_eq!(replaying.source_continuity_gap_total(), 0);
|
|
assert!(reporter.observe_source_continuity(crate::RawTransactionIngestSourceState::Active, 1, 1, 1, 0, 0).is_ok());
|
|
let gap = reporter.observe_source_continuity(crate::RawTransactionIngestSourceState::Reconnecting, 1, 2, 1, 0, 1);
|
|
assert!(gap.is_err());
|
|
let gap = match gap {
|
|
std::result::Result::Ok(()) => return,
|
|
std::result::Result::Err(value) => value,
|
|
};
|
|
assert_eq!(gap.code(), crate::ERROR_CODE_RAW_TRANSACTION_INGEST_RUNTIME_INVALID);
|
|
assert!(gap.context().iter().any(|context| {
|
|
return context.value() == "source.continuity_gap_proven";
|
|
}));
|
|
let proven = receiver.borrow().clone();
|
|
assert_eq!(proven.hydration_pending(), 1);
|
|
assert_eq!(proven.oldest_pending_slot(), std::option::Option::Some(42));
|
|
assert_eq!(proven.source_reconnect_total(), 1);
|
|
assert_eq!(proven.source_replay_attempt_total(), 2);
|
|
assert_eq!(proven.source_continuity_gap_total(), 1);
|
|
let regression = reporter.observe_source_continuity(crate::RawTransactionIngestSourceState::Active, 0, 2, 1, 0, 1);
|
|
assert!(regression.is_err());
|
|
let regression = match regression {
|
|
std::result::Result::Ok(()) => return,
|
|
std::result::Result::Err(value) => value,
|
|
};
|
|
assert!(regression.context().iter().any(|context| {
|
|
return context.value() == "source.continuity_counter_regression";
|
|
}));
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn v0_3_14_pre_004_replay_delivery_is_not_coverage_and_unproven_coverage_fails_closed() {
|
|
let (sender, receiver) = tokio::sync::watch::channel(crate::RawTransactionIngestProcessingFrontierProjection::empty());
|
|
let mut reporter = super::RawTransactionIngestProcessingFrontierReporter::new(sender);
|
|
assert!(reporter.observe_source_continuity(crate::RawTransactionIngestSourceState::Active, 1, 1, 0, 0, 0).is_ok());
|
|
let attempted = receiver.borrow().clone();
|
|
assert_eq!(attempted.source_reconnect_total(), 1);
|
|
assert_eq!(attempted.source_replay_attempt_total(), 1);
|
|
assert_eq!(attempted.source_continuity_gap_total(), 0);
|
|
let unproven = reporter.observe_source_continuity(crate::RawTransactionIngestSourceState::Active, 1, 1, 1, 1, 0);
|
|
assert!(unproven.is_err());
|
|
let unproven = match unproven {
|
|
std::result::Result::Ok(()) => return,
|
|
std::result::Result::Err(value) => value,
|
|
};
|
|
assert!(unproven.context().iter().any(|context| {
|
|
return context.value() == "source.replay_coverage_unproven";
|
|
}));
|
|
let regression = reporter.observe_source_continuity(crate::RawTransactionIngestSourceState::Active, 1, 1, 0, 1, 0);
|
|
assert!(regression.is_err());
|
|
let regression = match regression {
|
|
std::result::Result::Ok(()) => return,
|
|
std::result::Result::Err(value) => value,
|
|
};
|
|
assert!(regression.context().iter().any(|context| {
|
|
return context.value() == "source.continuity_counter_regression";
|
|
}));
|
|
return;
|
|
}
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn pre_009_duplicate_storm_is_bounded_coalesced_and_abort_leaves_no_orphan_frontier() {
|
|
let source = match signal_source() {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let settings = match pre_004_settings() {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let signal = match pre_004_signal(&source, super::RawTransactionIngestSourceFamily::Transaction, 10, 1, &["storm"], 0x55) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let received_at = match ksp_store_lib::RawTimestamp::from_unix_millis(1_760_000_300_000) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let mut coordinator = super::RawTransactionIngestHydrationCoordinator::new(&settings);
|
|
coordinator.max_in_flight = 0;
|
|
coordinator.max_pending_signals = 3;
|
|
let (frontier_sender, frontier_receiver) = tokio::sync::watch::channel(crate::RawTransactionIngestProcessingFrontierProjection::empty());
|
|
let mut processing_frontier = super::RawTransactionIngestProcessingFrontierReporter::new(frontier_sender);
|
|
let hydration = source.hydration_context();
|
|
assert!(processing_frontier.observe_settled(9).is_ok());
|
|
assert!(processing_frontier.observe_settled(10).is_ok());
|
|
for _ in 0..3 {
|
|
assert!(coordinator.queue_signal(&hydration, signal.clone(), received_at, &mut processing_frontier).is_ok());
|
|
}
|
|
let saturated = coordinator.queue_signal(&hydration, signal, received_at, &mut processing_frontier);
|
|
let saturated = match saturated {
|
|
std::result::Result::Ok(()) => return,
|
|
std::result::Result::Err(value) => value,
|
|
};
|
|
assert!(saturated.context().iter().any(|context| {
|
|
return context.value() == "source.hydration_pending_saturated";
|
|
}));
|
|
assert_eq!(coordinator.pending.len(), 1);
|
|
assert_eq!(coordinator.pending_signal_count, 3);
|
|
assert!(coordinator.tasks.is_empty());
|
|
let pending = frontier_receiver.borrow().clone();
|
|
assert_eq!(pending.hydration_pending(), 3);
|
|
assert_eq!(pending.oldest_pending_slot(), std::option::Option::Some(10));
|
|
assert!(pending.processing_frontier_slot() != std::option::Option::Some(10));
|
|
let safe_frontier_before_abort = pending.processing_frontier_slot();
|
|
coordinator.abort_all(&mut processing_frontier).await;
|
|
assert!(coordinator.pending.is_empty());
|
|
assert_eq!(coordinator.pending_signal_count, 0);
|
|
assert!(coordinator.tasks.is_empty());
|
|
let cleaned = frontier_receiver.borrow().clone();
|
|
assert_eq!(cleaned.hydration_pending(), 0);
|
|
assert_eq!(cleaned.oldest_pending_slot(), std::option::Option::None);
|
|
assert_eq!(cleaned.processing_frontier_slot(), safe_frontier_before_abort);
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn pre_009_frontier_counter_exhaustion_is_transactional_before_abort_cleanup() {
|
|
let mut frontier = super::RawTransactionIngestProcessingFrontier::new();
|
|
frontier.pending_total = 1;
|
|
frontier.slots.insert(77, super::RawTransactionIngestProcessingSlotState { pending: 1, settled: u64::MAX });
|
|
let before = frontier.projection();
|
|
let exhausted = frontier.settle_pending(77);
|
|
let exhausted = match exhausted {
|
|
std::result::Result::Ok(()) => return,
|
|
std::result::Result::Err(value) => value,
|
|
};
|
|
assert!(exhausted.context().iter().any(|context| {
|
|
return context.value() == "source.frontier_slot_settled_counter_exhausted";
|
|
}));
|
|
let after = frontier.projection();
|
|
assert_eq!(after.hydration_pending(), before.hydration_pending());
|
|
assert_eq!(after.oldest_pending_slot(), before.oldest_pending_slot());
|
|
assert_eq!(after.processing_frontier_slot(), before.processing_frontier_slot());
|
|
let state = match frontier.slots.get(&77) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
assert_eq!(state.pending, 1);
|
|
assert_eq!(state.settled, u64::MAX);
|
|
frontier.discard_all_pending();
|
|
let cleaned = frontier.projection();
|
|
assert_eq!(cleaned.hydration_pending(), 0);
|
|
assert_eq!(cleaned.oldest_pending_slot(), std::option::Option::None);
|
|
assert_eq!(cleaned.processing_frontier_slot(), std::option::Option::None);
|
|
return;
|
|
}
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn pre_009_abort_joins_in_flight_hydration_and_clears_pending_projection() {
|
|
let source = match signal_source() {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let settings = match pre_004_settings() {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let signal = match pre_004_signal(&source, super::RawTransactionIngestSourceFamily::TransactionStatus, 81, 2, &["stop-hydration"], 0x66) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let received_at = match ksp_store_lib::RawTimestamp::from_unix_millis(1_760_000_301_000) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let mut coordinator = super::RawTransactionIngestHydrationCoordinator::new(&settings);
|
|
let (frontier_sender, frontier_receiver) = tokio::sync::watch::channel(crate::RawTransactionIngestProcessingFrontierProjection::empty());
|
|
let mut processing_frontier = super::RawTransactionIngestProcessingFrontierReporter::new(frontier_sender);
|
|
let hydration = source.hydration_context();
|
|
assert!(coordinator.queue_signal(&hydration, signal, received_at, &mut processing_frontier).is_ok());
|
|
assert_eq!(coordinator.pending_signal_count, 1);
|
|
assert_eq!(coordinator.tasks.len(), 1);
|
|
assert_eq!(frontier_receiver.borrow().hydration_pending(), 1);
|
|
coordinator.abort_all(&mut processing_frontier).await;
|
|
assert_eq!(coordinator.pending_signal_count, 0);
|
|
assert!(coordinator.pending.is_empty());
|
|
assert!(coordinator.tasks.is_empty());
|
|
let cleaned = frontier_receiver.borrow().clone();
|
|
assert_eq!(cleaned.hydration_pending(), 0);
|
|
assert_eq!(cleaned.oldest_pending_slot(), std::option::Option::None);
|
|
assert_eq!(cleaned.processing_frontier_slot(), std::option::Option::None);
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn v0_3_13_pre_006_http_block_polling_source_validates_bounds_capabilities_and_logical_identity() {
|
|
let pool = match http_polling_pool("devnet") {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let source = match crate::RawTransactionIngestHttpBlockPollingSource::new(
|
|
pool.clone(),
|
|
ksp_onchain_transport_lib::HttpRoleName::new("live-poll"),
|
|
ksp_onchain_transport_lib::SolanaCommitment::Confirmed,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let same_identity_different_timing = match crate::RawTransactionIngestHttpBlockPollingSource::new_with_limits(
|
|
pool,
|
|
ksp_onchain_transport_lib::HttpRoleName::new("live-poll"),
|
|
ksp_onchain_transport_lib::SolanaCommitment::Confirmed,
|
|
std::time::Duration::from_secs(2),
|
|
256,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
assert_eq!(source.source_key, same_identity_different_timing.source_key);
|
|
assert_eq!(source.network.as_str(), "devnet");
|
|
assert_eq!(source.poll_interval, crate::DEFAULT_RAW_TRANSACTION_INGEST_HTTP_POLL_INTERVAL);
|
|
assert_eq!(source.max_discovered_blocks_per_cycle, crate::DEFAULT_RAW_TRANSACTION_INGEST_HTTP_POLL_MAX_BLOCKS_PER_CYCLE);
|
|
let different_endpoint_pool = match http_polling_pool_with_identity(
|
|
"devnet",
|
|
"live-poll",
|
|
"poll-fixture-other",
|
|
"fixture-provider",
|
|
&["get_block", "get_blocks_with_limit", "get_slot"],
|
|
) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let different_endpoint = match crate::RawTransactionIngestHttpBlockPollingSource::new(
|
|
different_endpoint_pool,
|
|
ksp_onchain_transport_lib::HttpRoleName::new("live-poll"),
|
|
ksp_onchain_transport_lib::SolanaCommitment::Confirmed,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
assert_ne!(source.source_key, different_endpoint.source_key);
|
|
let processed = crate::RawTransactionIngestHttpBlockPollingSource::new(
|
|
match http_polling_pool("devnet") {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
},
|
|
ksp_onchain_transport_lib::HttpRoleName::new("live-poll"),
|
|
ksp_onchain_transport_lib::SolanaCommitment::Processed,
|
|
);
|
|
assert!(processed.is_err());
|
|
let too_fast = crate::RawTransactionIngestHttpBlockPollingSource::new_with_limits(
|
|
match http_polling_pool("devnet") {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
},
|
|
ksp_onchain_transport_lib::HttpRoleName::new("live-poll"),
|
|
ksp_onchain_transport_lib::SolanaCommitment::Confirmed,
|
|
std::time::Duration::from_millis(99),
|
|
128,
|
|
);
|
|
assert!(too_fast.is_err());
|
|
let too_slow = crate::RawTransactionIngestHttpBlockPollingSource::new_with_limits(
|
|
match http_polling_pool("devnet") {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
},
|
|
ksp_onchain_transport_lib::HttpRoleName::new("live-poll"),
|
|
ksp_onchain_transport_lib::SolanaCommitment::Confirmed,
|
|
std::time::Duration::from_millis(30_001),
|
|
128,
|
|
);
|
|
assert!(too_slow.is_err());
|
|
for invalid_limit in [0_u16, 1025_u16] {
|
|
let invalid = crate::RawTransactionIngestHttpBlockPollingSource::new_with_limits(
|
|
match http_polling_pool("devnet") {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
},
|
|
ksp_onchain_transport_lib::HttpRoleName::new("live-poll"),
|
|
ksp_onchain_transport_lib::SolanaCommitment::Confirmed,
|
|
std::time::Duration::from_secs(1),
|
|
invalid_limit,
|
|
);
|
|
assert!(invalid.is_err());
|
|
}
|
|
let missing_capability_pool = match http_polling_pool_with_identity("devnet", "live-poll", "poll-missing", "fixture-provider", &["get_block", "get_slot"]) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let missing_capability = crate::RawTransactionIngestHttpBlockPollingSource::new(
|
|
missing_capability_pool,
|
|
ksp_onchain_transport_lib::HttpRoleName::new("live-poll"),
|
|
ksp_onchain_transport_lib::SolanaCommitment::Confirmed,
|
|
);
|
|
assert!(missing_capability.is_err());
|
|
let debug = std::format!("{source:?}");
|
|
assert!(!debug.contains("HTTP-POLL-SECRET-CANARY"));
|
|
assert!(!debug.contains("fixture.invalid"));
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn v0_3_13_pre_006_http_block_polling_discovery_never_moves_before_run_frontier_and_requires_strict_order() {
|
|
assert!(super::validate_http_block_polling_discovery(100, &[]).is_ok());
|
|
assert!(super::validate_http_block_polling_discovery(100, &[100, 102, 105]).is_ok());
|
|
assert!(super::validate_http_block_polling_discovery(100, &[99]).is_err());
|
|
assert!(super::validate_http_block_polling_discovery(100, &[100, 100]).is_err());
|
|
assert!(super::validate_http_block_polling_discovery(100, &[102, 101]).is_err());
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn v0_3_14_pre_006_http_scan_capabilities_prefer_closed_range_and_require_safe_fallback() {
|
|
let exact = super::RawTransactionIngestHttpScanCapabilities { get_block: true, get_blocks: true, get_blocks_with_limit: true, get_slot: true };
|
|
assert!(exact.can_scan());
|
|
let exact_strategy = match exact.strategy() {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
assert_eq!(exact_strategy, super::RawTransactionIngestHttpDiscoveryStrategy::ClosedRange);
|
|
let fallback = super::RawTransactionIngestHttpScanCapabilities { get_block: true, get_blocks: false, get_blocks_with_limit: true, get_slot: true };
|
|
assert!(fallback.can_scan());
|
|
let fallback_strategy = match fallback.strategy() {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
assert_eq!(fallback_strategy, super::RawTransactionIngestHttpDiscoveryStrategy::WithLimitBoundary);
|
|
for incomplete in [
|
|
super::RawTransactionIngestHttpScanCapabilities { get_block: false, get_blocks: true, get_blocks_with_limit: true, get_slot: true },
|
|
super::RawTransactionIngestHttpScanCapabilities { get_block: true, get_blocks: false, get_blocks_with_limit: true, get_slot: false },
|
|
super::RawTransactionIngestHttpScanCapabilities { get_block: true, get_blocks: false, get_blocks_with_limit: false, get_slot: true },
|
|
] {
|
|
assert!(!incomplete.can_scan());
|
|
assert!(incomplete.strategy().is_err());
|
|
}
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn v0_3_14_pre_006_http_scan_capability_detection_is_role_local_and_io_free() {
|
|
let cases = [
|
|
(&["get_block", "get_blocks"][..], std::option::Option::Some(super::RawTransactionIngestHttpDiscoveryStrategy::ClosedRange)),
|
|
(
|
|
&["get_block", "get_blocks_with_limit", "get_slot"][..],
|
|
std::option::Option::Some(super::RawTransactionIngestHttpDiscoveryStrategy::WithLimitBoundary),
|
|
),
|
|
(&["get_transaction"][..], std::option::Option::None),
|
|
];
|
|
for (request_kinds, expected_strategy) in cases {
|
|
let pool = match http_polling_pool_with_identity("devnet", "scan", "scan-fixture", "fixture-provider", request_kinds) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let capabilities = match super::http_role_scan_capabilities(&pool, &ksp_onchain_transport_lib::HttpRoleName::new("scan"), "devnet") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
match expected_strategy {
|
|
std::option::Option::Some(expected) => {
|
|
assert!(capabilities.can_scan());
|
|
let actual = match capabilities.strategy() {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
assert_eq!(actual, expected);
|
|
},
|
|
std::option::Option::None => {
|
|
assert!(!capabilities.can_scan());
|
|
assert!(capabilities.strategy().is_err());
|
|
},
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn v0_3_14_pre_006_http_discovery_window_is_inclusive_bounded_and_overflow_safe() {
|
|
let single = match super::http_discovery_window_slot_count(100, 100) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
assert_eq!(single, 1);
|
|
let maximum = match super::http_discovery_window_slot_count(100, 611) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
assert_eq!(maximum, crate::MAX_RAW_TRANSACTION_INGEST_CONTINUITY_DISCOVERY_WINDOW_SLOTS);
|
|
assert!(super::http_discovery_window_slot_count(100, 612).is_err());
|
|
assert!(super::http_discovery_window_slot_count(101, 100).is_err());
|
|
assert!(super::http_discovery_window_slot_count(0, u64::MAX).is_err());
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn v0_3_14_pre_006_closed_range_discovery_proves_skipped_slots_only_inside_exact_window() {
|
|
let complete = match super::validate_http_block_discovery_result(
|
|
100,
|
|
105,
|
|
super::RawTransactionIngestHttpDiscoveryStrategy::ClosedRange,
|
|
std::option::Option::None,
|
|
&[100, 102, 105],
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
assert_eq!(complete.produced_slots, std::vec![100, 102, 105]);
|
|
assert_eq!(complete.proven_end_slot, std::option::Option::Some(105));
|
|
let all_skipped = match super::validate_http_block_discovery_result(
|
|
200,
|
|
203,
|
|
super::RawTransactionIngestHttpDiscoveryStrategy::ClosedRange,
|
|
std::option::Option::None,
|
|
&[],
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
assert!(all_skipped.produced_slots.is_empty());
|
|
assert_eq!(all_skipped.proven_end_slot, std::option::Option::Some(203));
|
|
assert!(
|
|
super::validate_http_block_discovery_result(
|
|
100,
|
|
105,
|
|
super::RawTransactionIngestHttpDiscoveryStrategy::ClosedRange,
|
|
std::option::Option::None,
|
|
&[100, 106],
|
|
)
|
|
.is_err()
|
|
);
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn v0_3_14_pre_006_with_limit_discovery_never_uses_tip_alone_as_tail_coverage_proof() {
|
|
let crossed = match super::validate_http_block_discovery_result(
|
|
100,
|
|
105,
|
|
super::RawTransactionIngestHttpDiscoveryStrategy::WithLimitBoundary,
|
|
std::option::Option::Some(110),
|
|
&[100, 103, 107],
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
assert_eq!(crossed.produced_slots, std::vec![100, 103]);
|
|
assert_eq!(crossed.proven_end_slot, std::option::Option::Some(105));
|
|
let partial = match super::validate_http_block_discovery_result(
|
|
100,
|
|
105,
|
|
super::RawTransactionIngestHttpDiscoveryStrategy::WithLimitBoundary,
|
|
std::option::Option::Some(110),
|
|
&[100, 103],
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
assert_eq!(partial.produced_slots, std::vec![100, 103]);
|
|
assert_eq!(partial.proven_end_slot, std::option::Option::Some(103));
|
|
let tip_only = match super::validate_http_block_discovery_result(
|
|
100,
|
|
105,
|
|
super::RawTransactionIngestHttpDiscoveryStrategy::WithLimitBoundary,
|
|
std::option::Option::Some(110),
|
|
&[],
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
assert!(tip_only.produced_slots.is_empty());
|
|
assert_eq!(tip_only.proven_end_slot, std::option::Option::None);
|
|
assert!(
|
|
super::validate_http_block_discovery_result(
|
|
100,
|
|
105,
|
|
super::RawTransactionIngestHttpDiscoveryStrategy::WithLimitBoundary,
|
|
std::option::Option::None,
|
|
&[100, 103, 107],
|
|
)
|
|
.is_err()
|
|
);
|
|
return;
|
|
}
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn v0_3_14_pre_007_known_reference_hydration_reuses_global_registry_and_available_material() {
|
|
let body = std::format!(
|
|
"{{\"jsonrpc\":\"2.0\",\"result\":{{\"slot\":42,\"blockTime\":1760000120,\"transaction\":[\"{}\",\"base64\"],\"meta\":null,\"version\":0,\"transactionIndex\":7}},\"id\":1}}",
|
|
PRE_004_ZERO_TRANSACTION_BASE64,
|
|
);
|
|
let (url, server) = match serve_http_once(body) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let source = match signal_source_for_http_url(url.as_str()) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let settings = match pre_004_settings() {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let signal = match pre_004_signal(&source, super::RawTransactionIngestSourceFamily::Transaction, 42, 7, &["tx-fixture"], 0) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let hydration = source.hydration_context();
|
|
let key = match super::hydration_key(&hydration, &signal) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let registry = std::sync::Arc::new(super::RawTransactionIngestGlobalHydrationRegistry::new(4, 1));
|
|
let first = super::fetch_hydration_shared(
|
|
std::sync::Arc::clone(®istry),
|
|
hydration.http_pool.clone(),
|
|
hydration.hydration_role.clone(),
|
|
hydration.network.clone(),
|
|
key.clone(),
|
|
hydration.commitment,
|
|
);
|
|
let second = super::fetch_hydration_shared(
|
|
std::sync::Arc::clone(®istry),
|
|
hydration.http_pool.clone(),
|
|
hydration.hydration_role.clone(),
|
|
hydration.network.clone(),
|
|
key,
|
|
hydration.commitment,
|
|
);
|
|
let (first, second) = tokio::join!(first, second);
|
|
let first = match first {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
assert!(second.is_ok());
|
|
let request = match server.join() {
|
|
std::result::Result::Ok(std::result::Result::Ok(value)) => value,
|
|
_ => return,
|
|
};
|
|
assert!(request.contains("\"method\":\"getTransaction\""));
|
|
let received_at = match ksp_store_lib::RawTimestamp::from_unix_millis(1_760_000_200_000) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let resolved = super::resolve_known_reference_hydration(&hydration, &settings, signal, received_at, &first.observed);
|
|
let ingress = match resolved {
|
|
std::result::Result::Ok(super::RawTransactionIngestKnownReferenceHydrationResolution::Available(value)) => value,
|
|
_ => return,
|
|
};
|
|
let (mut admission, sender) = crate::RawTransactionAdmission::new(1);
|
|
if sender.send(*ingress).await.is_err() {
|
|
return;
|
|
}
|
|
std::mem::drop(sender);
|
|
let acquisition = match admission.receive(settings.network()).await {
|
|
std::result::Result::Ok(std::option::Option::Some(value)) => value,
|
|
_ => return,
|
|
};
|
|
assert_eq!(acquisition.transaction().slot(), 42);
|
|
assert_eq!(acquisition.transaction().reference().signature().as_bytes(), &[0_u8; 64]);
|
|
return;
|
|
}
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn v0_3_14_pre_007_missing_known_reference_remains_open_without_block_capability() {
|
|
let (url, server) = match serve_http_once("{\"jsonrpc\":\"2.0\",\"result\":null,\"id\":1}".to_owned()) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let source = match signal_source_for_http_url(url.as_str()) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let settings = match pre_004_settings() {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let signal = match pre_004_signal(&source, super::RawTransactionIngestSourceFamily::TransactionStatus, 77, 0, &["status-fixture"], 0) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let hydration = source.hydration_context();
|
|
let key = match super::hydration_key(&hydration, &signal) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let registry = std::sync::Arc::new(super::RawTransactionIngestGlobalHydrationRegistry::new(2, 1));
|
|
let fetched = super::fetch_hydration_shared(
|
|
registry,
|
|
hydration.http_pool.clone(),
|
|
hydration.hydration_role.clone(),
|
|
hydration.network.clone(),
|
|
key,
|
|
hydration.commitment,
|
|
)
|
|
.await;
|
|
let fetched = match fetched {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let _request = match server.join() {
|
|
std::result::Result::Ok(std::result::Result::Ok(value)) => value,
|
|
_ => return,
|
|
};
|
|
let received_at = match ksp_store_lib::RawTimestamp::from_unix_millis(1_760_000_200_000) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let resolved = super::resolve_known_reference_hydration(&hydration, &settings, signal, received_at, &fetched.observed);
|
|
let (disposition, obligation) = match resolved {
|
|
std::result::Result::Ok(super::RawTransactionIngestKnownReferenceHydrationResolution::Missing { disposition, obligation }) => (disposition, obligation),
|
|
_ => return,
|
|
};
|
|
assert_eq!(disposition, super::RawTransactionIngestKnownReferenceMissingDisposition::AwaitCoverage);
|
|
assert_eq!(obligation.slot(), 77);
|
|
assert_eq!(obligation.commitment(), ksp_onchain_transport_lib::SolanaCommitment::Confirmed);
|
|
assert_eq!(obligation.reference().signature().as_bytes(), &[0_u8; 64]);
|
|
return;
|
|
}
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn v0_3_14_pre_007_missing_known_reference_prefers_same_role_block_slot_when_supported() {
|
|
let (url, server) = match serve_http_once("{\"jsonrpc\":\"2.0\",\"result\":null,\"id\":1}".to_owned()) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let source = match signal_source_for_http_url_with_request_kinds(url.as_str(), &["get_transaction", "get_block"]) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let settings = match pre_004_settings() {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let signal = match pre_004_signal(&source, super::RawTransactionIngestSourceFamily::Transaction, 88, 0, &["tx-fixture"], 0) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let hydration = source.hydration_context();
|
|
let key = match super::hydration_key(&hydration, &signal) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let registry = std::sync::Arc::new(super::RawTransactionIngestGlobalHydrationRegistry::new(2, 1));
|
|
let fetched = super::fetch_hydration_shared(
|
|
registry,
|
|
hydration.http_pool.clone(),
|
|
hydration.hydration_role.clone(),
|
|
hydration.network.clone(),
|
|
key,
|
|
hydration.commitment,
|
|
)
|
|
.await;
|
|
let fetched = match fetched {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let _request = match server.join() {
|
|
std::result::Result::Ok(std::result::Result::Ok(value)) => value,
|
|
_ => return,
|
|
};
|
|
let received_at = match ksp_store_lib::RawTimestamp::from_unix_millis(1_760_000_200_000) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let resolved = super::resolve_known_reference_hydration(&hydration, &settings, signal, received_at, &fetched.observed);
|
|
let (disposition, obligation) = match resolved {
|
|
std::result::Result::Ok(super::RawTransactionIngestKnownReferenceHydrationResolution::Missing { disposition, obligation }) => (disposition, obligation),
|
|
_ => return,
|
|
};
|
|
assert_eq!(disposition, super::RawTransactionIngestKnownReferenceMissingDisposition::PreferBlockSlot);
|
|
assert_eq!(obligation.slot(), 88);
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn v0_3_13_pre_006_http_block_polling_qualifies_legacy_v0_v1_and_rejects_ambiguous_or_future_versions() {
|
|
let cases = [
|
|
(ksp_onchain_transport_lib::SolanaTransactionVersion::Legacy, ksp_raw_transaction_lib::RawTransactionVersion::Legacy),
|
|
(ksp_onchain_transport_lib::SolanaTransactionVersion::Number(0), ksp_raw_transaction_lib::RawTransactionVersion::Number(0)),
|
|
(ksp_onchain_transport_lib::SolanaTransactionVersion::Number(1), ksp_raw_transaction_lib::RawTransactionVersion::Number(1)),
|
|
];
|
|
for (wire, expected) in cases {
|
|
let qualified = super::qualify_http_block_polling_version(&ksp_onchain_transport_lib::SolanaWireField::Value(wire));
|
|
let qualified = match qualified {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let qualified = match qualified {
|
|
ksp_raw_transaction_lib::RawTransactionWireField::Value(value) => value,
|
|
_ => return,
|
|
};
|
|
assert_eq!(qualified, expected);
|
|
}
|
|
assert!(
|
|
super::qualify_http_block_polling_version(&ksp_onchain_transport_lib::SolanaWireField::Value(
|
|
ksp_onchain_transport_lib::SolanaTransactionVersion::Number(2),
|
|
))
|
|
.is_err()
|
|
);
|
|
assert!(super::qualify_http_block_polling_version(&ksp_onchain_transport_lib::SolanaWireField::Omitted).is_err());
|
|
assert!(super::qualify_http_block_polling_version(&ksp_onchain_transport_lib::SolanaWireField::Null).is_err());
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn v0_3_13_pre_006_runtime_resources_reject_duplicate_http_polling_identity_even_when_timing_differs() {
|
|
let first = match crate::RawTransactionIngestHttpBlockPollingSource::new(
|
|
match http_polling_pool("devnet") {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
},
|
|
ksp_onchain_transport_lib::HttpRoleName::new("live-poll"),
|
|
ksp_onchain_transport_lib::SolanaCommitment::Confirmed,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let duplicate = match crate::RawTransactionIngestHttpBlockPollingSource::new_with_limits(
|
|
match http_polling_pool("devnet") {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
},
|
|
ksp_onchain_transport_lib::HttpRoleName::new("live-poll"),
|
|
ksp_onchain_transport_lib::SolanaCommitment::Confirmed,
|
|
std::time::Duration::from_secs(2),
|
|
256,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let mut resources = crate::RawTransactionIngestRuntimeResources::from_http_block_polling_source(first);
|
|
let pushed = resources.try_push_http_block_polling_source(duplicate);
|
|
assert!(pushed.is_err());
|
|
assert_eq!(resources.source_count(), 1);
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn v0_3_13_pre_008_global_hydration_registry_coalesces_cross_source_key_to_one_leader() {
|
|
let settings = match pre_004_settings() {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let network = match ksp_store_lib::RawNetworkId::new("devnet") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let key = super::RawTransactionIngestHydrationKey {
|
|
commitment: ksp_onchain_transport_lib::SolanaCommitment::Confirmed.as_str(),
|
|
network,
|
|
signature: ksp_store_lib::RawTransactionSignature::new([61_u8; 64]),
|
|
};
|
|
let registry = super::RawTransactionIngestGlobalHydrationRegistry::new(settings.admission_queue_capacity(), settings.persistence_concurrency());
|
|
let (first_leader, _first_receiver) = match registry.subscribe_or_lead(&key) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let (second_leader, second_receiver) = match registry.subscribe_or_lead(&key) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
assert!(first_leader);
|
|
assert!(!second_leader);
|
|
registry.publish_and_remove(&key, super::RawTransactionIngestSharedHydrationResult::Failed(crate::ERROR_CODE_RAW_TRANSACTION_INGEST_SOURCE_FAILED));
|
|
let shared = second_receiver.borrow().clone();
|
|
match shared {
|
|
std::option::Option::Some(super::RawTransactionIngestSharedHydrationResult::Failed(code)) => {
|
|
assert_eq!(code, crate::ERROR_CODE_RAW_TRANSACTION_INGEST_SOURCE_FAILED);
|
|
},
|
|
_ => return,
|
|
}
|
|
let (third_leader, _third_receiver) = match registry.subscribe_or_lead(&key) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
assert!(third_leader);
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn v0_3_13_pre_009_hydration_quotas_partition_exact_bounds_and_reject_underprovision() {
|
|
assert!(super::validate_hydration_fairness_capacity(8, 5, 3).is_ok());
|
|
let pending_error = match super::validate_hydration_fairness_capacity(2, 5, 3) {
|
|
std::result::Result::Ok(()) => return,
|
|
std::result::Result::Err(value) => value,
|
|
};
|
|
assert_eq!(pending_error.context()[0].value(), "runtime_resources.hydration_pending_capacity_insufficient");
|
|
let concurrency_error = match super::validate_hydration_fairness_capacity(8, 2, 3) {
|
|
std::result::Result::Ok(()) => return,
|
|
std::result::Result::Err(value) => value,
|
|
};
|
|
assert_eq!(concurrency_error.context()[0].value(), "runtime_resources.hydration_concurrency_insufficient");
|
|
let pending_limits = [super::hydration_pending_limit(8, 3, 0), super::hydration_pending_limit(8, 3, 1), super::hydration_pending_limit(8, 3, 2)];
|
|
assert_eq!(pending_limits, [3, 3, 2]);
|
|
assert_eq!(pending_limits.iter().copied().sum::<usize>(), 8);
|
|
assert!(pending_limits.iter().copied().all(|limit| return limit >= 1));
|
|
let in_flight_limits = [super::hydration_in_flight_limit(5, 3, 0), super::hydration_in_flight_limit(5, 3, 1), super::hydration_in_flight_limit(5, 3, 2)];
|
|
assert_eq!(in_flight_limits, [2, 2, 1]);
|
|
assert_eq!(in_flight_limits.iter().copied().sum::<usize>(), 5);
|
|
assert!(in_flight_limits.iter().copied().all(|limit| return limit >= 1));
|
|
assert!(super::validate_hydration_fairness_capacity(1, 1, 0).is_ok());
|
|
assert_eq!(super::hydration_pending_limit(1, 0, 0), 0);
|
|
assert_eq!(super::hydration_in_flight_limit(1, 0, 0), 0);
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn v0_3_13_pre_009_duplicate_hydration_storm_keeps_one_global_key_and_distinct_keys_bounded() {
|
|
let network = match ksp_store_lib::RawNetworkId::new("devnet") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let registry = super::RawTransactionIngestGlobalHydrationRegistry::new(2, 1);
|
|
let first_key = super::RawTransactionIngestHydrationKey {
|
|
commitment: ksp_onchain_transport_lib::SolanaCommitment::Confirmed.as_str(),
|
|
network: network.clone(),
|
|
signature: ksp_store_lib::RawTransactionSignature::new([71_u8; 64]),
|
|
};
|
|
for index in 0..32 {
|
|
let subscribed = registry.subscribe_or_lead(&first_key);
|
|
let (leader, _receiver) = match subscribed {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
assert_eq!(leader, index == 0);
|
|
}
|
|
let second_key = super::RawTransactionIngestHydrationKey {
|
|
commitment: ksp_onchain_transport_lib::SolanaCommitment::Confirmed.as_str(),
|
|
network: network.clone(),
|
|
signature: ksp_store_lib::RawTransactionSignature::new([72_u8; 64]),
|
|
};
|
|
let third_key = super::RawTransactionIngestHydrationKey {
|
|
commitment: ksp_onchain_transport_lib::SolanaCommitment::Confirmed.as_str(),
|
|
network,
|
|
signature: ksp_store_lib::RawTransactionSignature::new([73_u8; 64]),
|
|
};
|
|
assert!(matches!(registry.subscribe_or_lead(&second_key), std::result::Result::Ok((true, _))));
|
|
let saturated = registry.subscribe_or_lead(&third_key);
|
|
let error = match saturated {
|
|
std::result::Result::Ok(_) => return,
|
|
std::result::Result::Err(value) => value,
|
|
};
|
|
assert_eq!(error.code(), crate::ERROR_CODE_RAW_TRANSACTION_INGEST_RUNTIME_INVALID);
|
|
assert_eq!(error.context()[0].value(), "source.global_hydration_pending_saturated");
|
|
return;
|
|
}
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn v0_3_13_pre_009_global_hydration_permit_and_leader_cleanup_are_bounded_and_fail_closed() {
|
|
let network = match ksp_store_lib::RawNetworkId::new("devnet") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let registry = std::sync::Arc::new(super::RawTransactionIngestGlobalHydrationRegistry::new(4, 1));
|
|
let first_permit = std::sync::Arc::clone(®istry.hydration_permits).try_acquire_owned();
|
|
let first_permit = match first_permit {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
assert!(std::sync::Arc::clone(®istry.hydration_permits).try_acquire_owned().is_err());
|
|
std::mem::drop(first_permit);
|
|
assert!(std::sync::Arc::clone(®istry.hydration_permits).try_acquire_owned().is_ok());
|
|
let key = super::RawTransactionIngestHydrationKey {
|
|
commitment: ksp_onchain_transport_lib::SolanaCommitment::Confirmed.as_str(),
|
|
network,
|
|
signature: ksp_store_lib::RawTransactionSignature::new([74_u8; 64]),
|
|
};
|
|
let subscribed = registry.subscribe_or_lead(&key);
|
|
let (leader, receiver) = match subscribed {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
assert!(leader);
|
|
{
|
|
let _guard = super::RawTransactionIngestHydrationLeaderGuard::new(std::sync::Arc::clone(®istry), key.clone());
|
|
}
|
|
let published = receiver.borrow().clone();
|
|
assert!(matches!(published, std::option::Option::Some(super::RawTransactionIngestSharedHydrationResult::Failed(_))));
|
|
let resubscribed = registry.subscribe_or_lead(&key);
|
|
assert!(matches!(resubscribed, std::result::Result::Ok((true, _))));
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn v0_3_13_pre_011_source_inventory_rejects_stale_identity_and_counter_exhaustion() {
|
|
let mut identity_inventory = super::RawTransactionIngestSourceInventory::new(std::vec![[1_u8; 32]]);
|
|
let mismatch = identity_inventory.update(0, [2_u8; 32], crate::RawTransactionIngestProcessingFrontierProjection::empty());
|
|
let mismatch = match mismatch {
|
|
std::result::Result::Ok(()) => return,
|
|
std::result::Result::Err(value) => value,
|
|
};
|
|
assert_eq!(mismatch.code(), crate::ERROR_CODE_RAW_TRANSACTION_INGEST_RUNTIME_INVALID);
|
|
assert_eq!(mismatch.context()[0].value(), "source.inventory_key_mismatch");
|
|
let missing = identity_inventory.update(1, [1_u8; 32], crate::RawTransactionIngestProcessingFrontierProjection::empty());
|
|
let missing = match missing {
|
|
std::result::Result::Ok(()) => return,
|
|
std::result::Result::Err(value) => value,
|
|
};
|
|
assert_eq!(missing.code(), crate::ERROR_CODE_RAW_TRANSACTION_INGEST_RUNTIME_INVALID);
|
|
assert_eq!(missing.context()[0].value(), "source.inventory_entry_missing");
|
|
let mut pending_inventory = super::RawTransactionIngestSourceInventory::new(std::vec![[3_u8; 32], [4_u8; 32]]);
|
|
assert!(
|
|
pending_inventory
|
|
.update(
|
|
0,
|
|
[3_u8; 32],
|
|
crate::RawTransactionIngestProcessingFrontierProjection::new(usize::MAX, std::option::Option::Some(10), std::option::Option::None),
|
|
)
|
|
.is_ok()
|
|
);
|
|
assert!(
|
|
pending_inventory
|
|
.update(1, [4_u8; 32], crate::RawTransactionIngestProcessingFrontierProjection::new(1, std::option::Option::Some(11), std::option::Option::None),)
|
|
.is_ok()
|
|
);
|
|
let pending_error = match pending_inventory.aggregate() {
|
|
std::result::Result::Ok(_) => return,
|
|
std::result::Result::Err(value) => value,
|
|
};
|
|
assert_eq!(pending_error.code(), crate::ERROR_CODE_RAW_TRANSACTION_INGEST_COUNTER_EXHAUSTED);
|
|
assert_eq!(pending_error.context()[0].value(), "hydration_pending");
|
|
let mut continuity_inventory = super::RawTransactionIngestSourceInventory::new(std::vec![[5_u8; 32], [6_u8; 32]]);
|
|
assert!(
|
|
continuity_inventory
|
|
.update(
|
|
0,
|
|
[5_u8; 32],
|
|
crate::RawTransactionIngestProcessingFrontierProjection::new(0, std::option::Option::Some(20), std::option::Option::None)
|
|
.with_source_continuity(std::option::Option::Some(crate::RawTransactionIngestSourceState::Active), u64::MAX, 0, 0),
|
|
)
|
|
.is_ok()
|
|
);
|
|
assert!(
|
|
continuity_inventory
|
|
.update(
|
|
1,
|
|
[6_u8; 32],
|
|
crate::RawTransactionIngestProcessingFrontierProjection::new(0, std::option::Option::Some(21), std::option::Option::None)
|
|
.with_source_continuity(std::option::Option::Some(crate::RawTransactionIngestSourceState::Active), 1, 0, 0),
|
|
)
|
|
.is_ok()
|
|
);
|
|
let continuity_error = match continuity_inventory.aggregate() {
|
|
std::result::Result::Ok(_) => return,
|
|
std::result::Result::Err(value) => value,
|
|
};
|
|
assert_eq!(continuity_error.code(), crate::ERROR_CODE_RAW_TRANSACTION_INGEST_COUNTER_EXHAUSTED);
|
|
assert_eq!(continuity_error.context()[0].value(), "source_reconnect_total");
|
|
return;
|
|
}
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn v0_3_13_pre_011_aborted_hydration_leader_notifies_followers_and_releases_registry_key() {
|
|
let network = match ksp_store_lib::RawNetworkId::new("devnet") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let registry = std::sync::Arc::new(super::RawTransactionIngestGlobalHydrationRegistry::new(2, 1));
|
|
let key = super::RawTransactionIngestHydrationKey {
|
|
commitment: ksp_onchain_transport_lib::SolanaCommitment::Confirmed.as_str(),
|
|
network,
|
|
signature: ksp_store_lib::RawTransactionSignature::new([91_u8; 64]),
|
|
};
|
|
let subscribed = registry.subscribe_or_lead(&key);
|
|
let (leader, follower) = match subscribed {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
assert!(leader);
|
|
let task_registry = std::sync::Arc::clone(®istry);
|
|
let task_key = key.clone();
|
|
let leader_task = tokio::spawn(async move {
|
|
let _guard = super::RawTransactionIngestHydrationLeaderGuard::new(task_registry, task_key);
|
|
return std::future::pending::<()>().await;
|
|
});
|
|
tokio::task::yield_now().await;
|
|
leader_task.abort();
|
|
let _joined = leader_task.await;
|
|
let published = follower.borrow().clone();
|
|
match published {
|
|
std::option::Option::Some(super::RawTransactionIngestSharedHydrationResult::Failed(code)) => {
|
|
assert_eq!(code, crate::ERROR_CODE_RAW_TRANSACTION_INGEST_SOURCE_FAILED);
|
|
},
|
|
_ => return,
|
|
}
|
|
assert!(matches!(registry.subscribe_or_lead(&key), std::result::Result::Ok((true, _))));
|
|
return;
|
|
}
|
|
|
|
struct Pre011SourceActiveGuard {
|
|
active: std::sync::Arc<std::sync::atomic::AtomicUsize>,
|
|
}
|
|
|
|
impl Pre011SourceActiveGuard {
|
|
fn new(active: std::sync::Arc<std::sync::atomic::AtomicUsize>) -> Self {
|
|
active.fetch_add(1, std::sync::atomic::Ordering::AcqRel);
|
|
return Self { active };
|
|
}
|
|
}
|
|
|
|
impl std::ops::Drop for Pre011SourceActiveGuard {
|
|
fn drop(&mut self) {
|
|
self.active.fetch_sub(1, std::sync::atomic::Ordering::AcqRel);
|
|
return;
|
|
}
|
|
}
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn v0_3_13_pre_011_aborting_outer_source_supervisor_aborts_nested_source_tasks() {
|
|
let active = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
|
let (_stop_sender, stop_receiver) = tokio::sync::watch::channel(false);
|
|
let (source_stop_sender, _source_stop_receiver) = tokio::sync::watch::channel(false);
|
|
let contracts = match supervisor_contracts(&[(1, "fixture-a", 1)]) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let inventory = match supervisor_inventory(std::vec![[1_u8; 32]], &[std::option::Option::Some(crate::RawTransactionIngestSourceState::Active)]) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let mut children = tokio::task::JoinSet::new();
|
|
let task_active = std::sync::Arc::clone(&active);
|
|
let _abort_handle = children.spawn(async move {
|
|
let _guard = Pre011SourceActiveGuard::new(task_active);
|
|
return std::future::pending::<([u8; 32], ksp_core_lib::Result<()>)>().await;
|
|
});
|
|
let supervisor = tokio::spawn(async move {
|
|
return super::supervise_live_source_tasks(
|
|
stop_receiver,
|
|
source_stop_sender,
|
|
children,
|
|
contracts,
|
|
inventory,
|
|
tokio::sync::watch::channel(crate::RawTransactionIngestProcessingFrontierProjection::empty()).0,
|
|
std::time::Duration::from_secs(5),
|
|
)
|
|
.await;
|
|
});
|
|
for _ in 0..64 {
|
|
if active.load(std::sync::atomic::Ordering::Acquire) == 1 {
|
|
break;
|
|
}
|
|
tokio::task::yield_now().await;
|
|
}
|
|
assert_eq!(active.load(std::sync::atomic::Ordering::Acquire), 1);
|
|
supervisor.abort();
|
|
let _joined = supervisor.await;
|
|
for _ in 0..64 {
|
|
if active.load(std::sync::atomic::Ordering::Acquire) == 0 {
|
|
break;
|
|
}
|
|
tokio::task::yield_now().await;
|
|
}
|
|
assert_eq!(active.load(std::sync::atomic::Ordering::Acquire), 0);
|
|
return;
|
|
}
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn v0_3_13_pre_011_stop_racing_ready_source_fault_preserves_fault_and_joins_sibling() {
|
|
let sibling_active = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
|
let fault_gate = std::sync::Arc::new(tokio::sync::Notify::new());
|
|
let (stop_sender, stop_receiver) = tokio::sync::watch::channel(false);
|
|
let (source_stop_sender, source_stop_receiver) = tokio::sync::watch::channel(false);
|
|
let contracts = match supervisor_contracts(&[(1, "fixture-a", 1), (2, "fixture-b", 2)]) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let inventory = match supervisor_inventory(
|
|
std::vec![[1_u8; 32], [2_u8; 32]],
|
|
&[
|
|
std::option::Option::Some(crate::RawTransactionIngestSourceState::Failed),
|
|
std::option::Option::Some(crate::RawTransactionIngestSourceState::Active),
|
|
],
|
|
) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let mut children = tokio::task::JoinSet::new();
|
|
let task_fault_gate = std::sync::Arc::clone(&fault_gate);
|
|
let _fault_abort_handle = children.spawn(async move {
|
|
task_fault_gate.notified().await;
|
|
return ([1_u8; 32], std::result::Result::Err(crate::runtime_error("test.pre_011_ready_source_fault")));
|
|
});
|
|
let sibling_active_for_task = std::sync::Arc::clone(&sibling_active);
|
|
let mut sibling_stop_receiver = source_stop_receiver.clone();
|
|
let _sibling_abort_handle = children.spawn(async move {
|
|
let _guard = Pre011SourceActiveGuard::new(sibling_active_for_task);
|
|
loop {
|
|
let changed = sibling_stop_receiver.changed().await;
|
|
if changed.is_err() || *sibling_stop_receiver.borrow() {
|
|
return ([2_u8; 32], std::result::Result::Ok(()));
|
|
}
|
|
}
|
|
});
|
|
let supervisor = tokio::spawn(async move {
|
|
return super::supervise_live_source_tasks(
|
|
stop_receiver,
|
|
source_stop_sender,
|
|
children,
|
|
contracts,
|
|
inventory,
|
|
tokio::sync::watch::channel(crate::RawTransactionIngestProcessingFrontierProjection::empty()).0,
|
|
std::time::Duration::from_secs(5),
|
|
)
|
|
.await;
|
|
});
|
|
for _ in 0..64 {
|
|
if sibling_active.load(std::sync::atomic::Ordering::Acquire) == 1 {
|
|
break;
|
|
}
|
|
tokio::task::yield_now().await;
|
|
}
|
|
assert_eq!(sibling_active.load(std::sync::atomic::Ordering::Acquire), 1);
|
|
stop_sender.send_replace(true);
|
|
fault_gate.notify_one();
|
|
let result = match supervisor.await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let error = match result {
|
|
std::result::Result::Ok(()) => return,
|
|
std::result::Result::Err(value) => value,
|
|
};
|
|
assert_eq!(error.context()[0].value(), "test.pre_011_ready_source_fault");
|
|
assert_eq!(sibling_active.load(std::sync::atomic::Ordering::Acquire), 0);
|
|
return;
|
|
}
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn v0_3_14_pre_012_source_fault_preserves_first_fault_and_aborts_non_cooperative_sibling() {
|
|
let active = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
|
let barrier = std::sync::Arc::new(tokio::sync::Barrier::new(2));
|
|
let (_stop_sender, stop_receiver) = tokio::sync::watch::channel(false);
|
|
let (source_stop_sender, _source_stop_receiver) = tokio::sync::watch::channel(false);
|
|
let contracts = match supervisor_contracts(&[(1, "fixture-a", 1), (2, "fixture-b", 2)]) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let inventory = match supervisor_inventory(
|
|
std::vec![[1_u8; 32], [2_u8; 32]],
|
|
&[
|
|
std::option::Option::Some(crate::RawTransactionIngestSourceState::Failed),
|
|
std::option::Option::Some(crate::RawTransactionIngestSourceState::Active),
|
|
],
|
|
) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let mut children = tokio::task::JoinSet::new();
|
|
let failing_barrier = std::sync::Arc::clone(&barrier);
|
|
let _failing_abort_handle = children.spawn(async move {
|
|
let _barrier_wait = failing_barrier.wait().await;
|
|
return ([1_u8; 32], std::result::Result::Err(crate::runtime_error("test.pre_012_source_fault")));
|
|
});
|
|
let sibling_barrier = std::sync::Arc::clone(&barrier);
|
|
let sibling_active = std::sync::Arc::clone(&active);
|
|
let _sibling_abort_handle = children.spawn(async move {
|
|
let _guard = Pre011SourceActiveGuard::new(sibling_active);
|
|
let _barrier_wait = sibling_barrier.wait().await;
|
|
return std::future::pending::<([u8; 32], ksp_core_lib::Result<()>)>().await;
|
|
});
|
|
let result = super::supervise_live_source_tasks(
|
|
stop_receiver,
|
|
source_stop_sender,
|
|
children,
|
|
contracts,
|
|
inventory,
|
|
tokio::sync::watch::channel(crate::RawTransactionIngestProcessingFrontierProjection::empty()).0,
|
|
std::time::Duration::from_millis(5),
|
|
)
|
|
.await;
|
|
let error = match result {
|
|
std::result::Result::Ok(()) => return,
|
|
std::result::Result::Err(value) => value,
|
|
};
|
|
assert_eq!(error.context()[0].value(), "test.pre_012_source_fault");
|
|
assert_eq!(active.load(std::sync::atomic::Ordering::Acquire), 0);
|
|
return;
|
|
}
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn v0_3_14_pre_012_stop_aborts_non_cooperative_source_after_bounded_drain() {
|
|
let active = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
|
let (stop_sender, stop_receiver) = tokio::sync::watch::channel(false);
|
|
let (source_stop_sender, _source_stop_receiver) = tokio::sync::watch::channel(false);
|
|
let contracts = match supervisor_contracts(&[(1, "fixture-a", 1)]) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let inventory = match supervisor_inventory(std::vec![[1_u8; 32]], &[std::option::Option::Some(crate::RawTransactionIngestSourceState::Active)]) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let mut children = tokio::task::JoinSet::new();
|
|
let source_active = std::sync::Arc::clone(&active);
|
|
let _abort_handle = children.spawn(async move {
|
|
let _guard = Pre011SourceActiveGuard::new(source_active);
|
|
return std::future::pending::<([u8; 32], ksp_core_lib::Result<()>)>().await;
|
|
});
|
|
let supervisor = tokio::spawn(async move {
|
|
return super::supervise_live_source_tasks(
|
|
stop_receiver,
|
|
source_stop_sender,
|
|
children,
|
|
contracts,
|
|
inventory,
|
|
tokio::sync::watch::channel(crate::RawTransactionIngestProcessingFrontierProjection::empty()).0,
|
|
std::time::Duration::from_millis(5),
|
|
)
|
|
.await;
|
|
});
|
|
for _ in 0..64 {
|
|
if active.load(std::sync::atomic::Ordering::Acquire) == 1 {
|
|
break;
|
|
}
|
|
tokio::task::yield_now().await;
|
|
}
|
|
assert_eq!(active.load(std::sync::atomic::Ordering::Acquire), 1);
|
|
stop_sender.send_replace(true);
|
|
let result = match supervisor.await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let error = match result {
|
|
std::result::Result::Ok(()) => return,
|
|
std::result::Result::Err(value) => value,
|
|
};
|
|
assert_eq!(error.code(), crate::ERROR_CODE_RAW_TRANSACTION_INGEST_DRAIN_TIMEOUT);
|
|
assert_eq!(active.load(std::sync::atomic::Ordering::Acquire), 0);
|
|
return;
|
|
}
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn v0_3_14_pre_008_proven_full_ledger_epoch_survives_filtered_peer_loss_without_worker_respawn() {
|
|
let sibling_active = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
|
|
let (stop_sender, stop_receiver) = tokio::sync::watch::channel(false);
|
|
let (source_stop_sender, source_stop_receiver) = tokio::sync::watch::channel(false);
|
|
let contracts = match supervisor_contracts(&[(1, "standard_logs", 7), (2, "full_ledger", 0)]) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
{
|
|
let mut contracts_guard = match contracts.lock() {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(poisoned) => poisoned.into_inner(),
|
|
};
|
|
assert!(contracts_guard.record_coverage_epoch([2_u8; 32], 90, 110).is_ok());
|
|
}
|
|
let inventory = match supervisor_inventory(
|
|
std::vec![[1_u8; 32], [2_u8; 32]],
|
|
&[
|
|
std::option::Option::Some(crate::RawTransactionIngestSourceState::Failed),
|
|
std::option::Option::Some(crate::RawTransactionIngestSourceState::Active),
|
|
],
|
|
) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let mut children = tokio::task::JoinSet::new();
|
|
let _lost_abort_handle = children.spawn(async move {
|
|
return ([1_u8; 32], std::result::Result::Err(super::continuity_range_error("source.continuity_gap_proven", 95, 100)));
|
|
});
|
|
let sibling_active_for_task = std::sync::Arc::clone(&sibling_active);
|
|
let mut sibling_stop_receiver = source_stop_receiver.clone();
|
|
let _sibling_abort_handle = children.spawn(async move {
|
|
sibling_active_for_task.store(true, std::sync::atomic::Ordering::Release);
|
|
loop {
|
|
let changed = sibling_stop_receiver.changed().await;
|
|
if changed.is_err() || *sibling_stop_receiver.borrow() {
|
|
sibling_active_for_task.store(false, std::sync::atomic::Ordering::Release);
|
|
return ([2_u8; 32], std::result::Result::Ok(()));
|
|
}
|
|
}
|
|
});
|
|
let (health_sender, health_receiver) = tokio::sync::watch::channel(crate::RawTransactionIngestProcessingFrontierProjection::empty());
|
|
let supervisor = tokio::spawn(async move {
|
|
return super::supervise_live_source_tasks(
|
|
stop_receiver,
|
|
source_stop_sender,
|
|
children,
|
|
contracts,
|
|
inventory,
|
|
health_sender,
|
|
std::time::Duration::from_secs(5),
|
|
)
|
|
.await;
|
|
});
|
|
for _ in 0..64 {
|
|
if sibling_active.load(std::sync::atomic::Ordering::Acquire) && health_receiver.borrow().failed_source_losses_reconciled() {
|
|
break;
|
|
}
|
|
tokio::task::yield_now().await;
|
|
}
|
|
assert!(sibling_active.load(std::sync::atomic::Ordering::Acquire));
|
|
let health = health_receiver.borrow().clone();
|
|
assert!(health.continuity_policy_observed());
|
|
assert!(!health.continuity_has_open_gaps());
|
|
assert!(health.future_target_coverage());
|
|
assert!(health.failed_source_losses_reconciled());
|
|
stop_sender.send_replace(true);
|
|
let result = match supervisor.await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
assert!(result.is_ok());
|
|
assert!(!sibling_active.load(std::sync::atomic::Ordering::Acquire));
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn v0_3_14_pre_008_only_classified_source_loss_enters_coverage_decision() {
|
|
let transport = super::source_transport_error(ksp_onchain_transport_lib::ERROR_CODE_HTTP_CONNECTION_FAILED);
|
|
assert!(super::source_loss_is_reconcilable(&transport));
|
|
assert!(super::source_loss_is_reconcilable(&crate::runtime_error("source.continuity_gap_proven")));
|
|
assert!(super::source_loss_is_reconcilable(&crate::runtime_error("source.replay_coverage_unproven")));
|
|
assert!(!super::source_loss_is_reconcilable(&crate::runtime_error("source.hydration_task_join_failed")));
|
|
let bounded = super::continuity_range_error("source.continuity_gap_proven", 10, 20);
|
|
assert!(matches!(super::source_loss_continuity_range(&bounded), std::result::Result::Ok(std::option::Option::Some((10, 20)))));
|
|
assert!(matches!(
|
|
super::source_loss_continuity_range(&crate::runtime_error("source.continuity_gap_proven")),
|
|
std::result::Result::Ok(std::option::Option::None)
|
|
));
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn v0_3_14_pre_003_websocket_reconnect_is_anchored_before_terminal_gap_projection() {
|
|
let (sender, receiver) = tokio::sync::watch::channel(crate::RawTransactionIngestProcessingFrontierProjection::empty());
|
|
let mut reporter = super::RawTransactionIngestProcessingFrontierReporter::new(sender);
|
|
assert!(reporter.observe_settled(100).is_ok());
|
|
assert!(reporter.observe_websocket_continuity(crate::RawTransactionIngestSourceState::Active, 0, 0).is_ok());
|
|
assert!(reporter.observe_websocket_continuity(crate::RawTransactionIngestSourceState::Reconnecting, 1, 0).is_ok());
|
|
let anchor = match reporter.websocket_incident_anchor {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => panic!("reconnect incident anchor missing"),
|
|
};
|
|
assert_eq!(anchor.start_slot(), 100);
|
|
assert_eq!(anchor.end_slot(), std::option::Option::None);
|
|
assert!(anchor.saw_reconnect());
|
|
assert!(!anchor.saw_overflow());
|
|
assert!(reporter.observe_websocket_continuity(crate::RawTransactionIngestSourceState::Active, 1, 0).is_ok());
|
|
let bounded = reporter.observe_websocket_post_incident_slot(105);
|
|
assert!(matches!(bounded, std::result::Result::Ok(std::option::Option::Some((100, 105)))));
|
|
let anchor = match reporter.websocket_incident_anchor {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => panic!("bounded reconnect incident anchor missing"),
|
|
};
|
|
assert_eq!(anchor.end_slot(), std::option::Option::Some(105));
|
|
let projection = receiver.borrow().clone();
|
|
assert_eq!(projection.source_reconnect_total(), 1);
|
|
assert_eq!(projection.source_replay_attempt_total(), 0);
|
|
assert_eq!(projection.source_continuity_gap_total(), 1);
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn v0_3_14_pre_003_websocket_overflow_and_reconnect_storm_share_earliest_anchor() {
|
|
let (sender, receiver) = tokio::sync::watch::channel(crate::RawTransactionIngestProcessingFrontierProjection::empty());
|
|
let mut reporter = super::RawTransactionIngestProcessingFrontierReporter::new(sender);
|
|
assert!(reporter.observe_settled(200).is_ok());
|
|
assert!(reporter.observe_websocket_continuity(crate::RawTransactionIngestSourceState::Active, 0, 1).is_ok());
|
|
assert!(reporter.observe_websocket_continuity(crate::RawTransactionIngestSourceState::Reconnecting, 1, 2).is_ok());
|
|
let anchor = match reporter.websocket_incident_anchor {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => panic!("overflow incident anchor missing"),
|
|
};
|
|
assert_eq!(anchor.start_slot(), 200);
|
|
assert_eq!(anchor.end_slot(), std::option::Option::None);
|
|
assert!(anchor.saw_reconnect());
|
|
assert!(anchor.saw_overflow());
|
|
let projection = receiver.borrow().clone();
|
|
assert_eq!(projection.source_reconnect_total(), 1);
|
|
assert_eq!(projection.source_continuity_gap_total(), 3);
|
|
let bounded = reporter.observe_websocket_post_incident_slot(201);
|
|
assert!(matches!(bounded, std::result::Result::Ok(std::option::Option::Some((200, 201)))));
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn v0_3_14_pre_003_websocket_incident_without_observed_slot_is_unbounded_and_counter_regression_fails() {
|
|
let (sender, _receiver) = tokio::sync::watch::channel(crate::RawTransactionIngestProcessingFrontierProjection::empty());
|
|
let mut reporter = super::RawTransactionIngestProcessingFrontierReporter::new(sender);
|
|
let unbounded = reporter.observe_websocket_continuity(crate::RawTransactionIngestSourceState::Reconnecting, 1, 0);
|
|
assert!(unbounded.is_err());
|
|
let unbounded = match unbounded {
|
|
std::result::Result::Ok(()) => return,
|
|
std::result::Result::Err(value) => value,
|
|
};
|
|
assert!(unbounded.context().iter().any(|context| {
|
|
return context.value() == "source.websocket_incident_unbounded";
|
|
}));
|
|
assert!(reporter.observe_websocket_continuity(crate::RawTransactionIngestSourceState::Active, 0, 0).is_err());
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn v0_3_14_pre_009_inventory_health_projection_tracks_reconciled_coverage() {
|
|
let source_keys = std::vec![[1_u8; 32], [2_u8; 32]];
|
|
let continuity_contracts = match supervisor_contracts(&[(1, "standard_logs", 7), (2, "standard_logs", 7)]) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let inventory = std::sync::Arc::new(std::sync::Mutex::new(super::RawTransactionIngestSourceInventory::new(source_keys)));
|
|
let (aggregate_sender, aggregate_receiver) = tokio::sync::watch::channel(crate::RawTransactionIngestProcessingFrontierProjection::empty());
|
|
let first = super::RawTransactionIngestSourceInventoryPublisher {
|
|
aggregate_sender: aggregate_sender.clone(),
|
|
continuity_contracts: std::sync::Arc::clone(&continuity_contracts),
|
|
entry_index: 0,
|
|
inventory: std::sync::Arc::clone(&inventory),
|
|
source_key: [1_u8; 32],
|
|
};
|
|
let second = super::RawTransactionIngestSourceInventoryPublisher {
|
|
aggregate_sender,
|
|
continuity_contracts: std::sync::Arc::clone(&continuity_contracts),
|
|
entry_index: 1,
|
|
inventory,
|
|
source_key: [2_u8; 32],
|
|
};
|
|
let active = crate::RawTransactionIngestProcessingFrontierProjection::new(0, std::option::Option::Some(60), std::option::Option::None)
|
|
.with_source_continuity(std::option::Option::Some(crate::RawTransactionIngestSourceState::Active), 0, 0, 0);
|
|
assert!(first.publish(active.clone()).is_ok());
|
|
assert!(second.publish(active.clone()).is_ok());
|
|
let healthy = aggregate_receiver.borrow().clone();
|
|
assert!(healthy.continuity_policy_observed());
|
|
assert_eq!(healthy.continuity_frontier_slot(), std::option::Option::Some(60));
|
|
assert!(!healthy.continuity_has_open_gaps());
|
|
assert!(healthy.future_target_coverage());
|
|
{
|
|
let mut contracts = match continuity_contracts.lock() {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(poisoned) => poisoned.into_inner(),
|
|
};
|
|
assert!(contracts.record_source_loss_gap([2_u8; 32], 50, 55).is_ok());
|
|
}
|
|
assert!(first.publish(active.clone()).is_ok());
|
|
let pending = aggregate_receiver.borrow().clone();
|
|
assert_eq!(pending.continuity_frontier_slot(), std::option::Option::Some(49));
|
|
assert!(pending.continuity_has_open_gaps());
|
|
assert!(pending.future_target_coverage());
|
|
assert_eq!(pending.continuity_snapshot().open_gap_count(), 1);
|
|
assert_eq!(pending.continuity_snapshot().oldest_open_gap_start_slot(), std::option::Option::Some(50));
|
|
assert_eq!(pending.continuity_snapshot().gaps().len(), 1);
|
|
{
|
|
let mut contracts = match continuity_contracts.lock() {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(poisoned) => poisoned.into_inner(),
|
|
};
|
|
assert!(contracts.record_coverage_epoch([1_u8; 32], 40, 60).is_ok());
|
|
}
|
|
assert!(first.publish(active.clone()).is_ok());
|
|
let reconciled = aggregate_receiver.borrow().clone();
|
|
assert_eq!(reconciled.continuity_frontier_slot(), std::option::Option::Some(60));
|
|
assert!(!reconciled.continuity_has_open_gaps());
|
|
assert!(reconciled.future_target_coverage());
|
|
assert_eq!(reconciled.continuity_snapshot().open_gap_count(), 0);
|
|
assert_eq!(reconciled.continuity_snapshot().repaired_gap_total(), 1);
|
|
assert_eq!(reconciled.continuity_snapshot().redundant_coverage_repair_total(), 1);
|
|
assert_eq!(
|
|
reconciled.continuity_snapshot().gaps()[0].last_method(),
|
|
std::option::Option::Some(crate::RawTransactionIngestRepairMethod::RedundantCoverage)
|
|
);
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn v0_3_14_pre_010_fair_scheduler_alternates_nominal_and_repair_with_bounded_burst() {
|
|
assert_eq!(super::MAX_RAW_TRANSACTION_INGEST_REPAIR_BURST, 1);
|
|
let mut state = super::RawTransactionIngestFairTurnState {
|
|
active: false,
|
|
last_granted: std::option::Option::None,
|
|
next_ticket: 3,
|
|
waiters: std::collections::VecDeque::from([
|
|
(1, super::RawTransactionIngestTrafficClass::Nominal),
|
|
(2, super::RawTransactionIngestTrafficClass::Repair),
|
|
]),
|
|
};
|
|
assert_eq!(super::fair_turn_selected_ticket(&state), std::option::Option::Some(1));
|
|
state.last_granted = std::option::Option::Some(super::RawTransactionIngestTrafficClass::Nominal);
|
|
assert_eq!(super::fair_turn_selected_ticket(&state), std::option::Option::Some(2));
|
|
state.last_granted = std::option::Option::Some(super::RawTransactionIngestTrafficClass::Repair);
|
|
assert_eq!(super::fair_turn_selected_ticket(&state), std::option::Option::Some(1));
|
|
let _removed = state.waiters.pop_front();
|
|
assert_eq!(super::fair_turn_selected_ticket(&state), std::option::Option::Some(2));
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn v0_3_14_pre_010_fair_waiter_bound_is_checked_and_released_without_reserved_capacity() {
|
|
assert!(super::validate_repair_fairness_contract(1, 1).is_ok());
|
|
assert_eq!(super::repair_block_fetch_limit(1), 1);
|
|
assert_eq!(super::repair_block_fetch_limit(4), 4);
|
|
assert_eq!(super::repair_block_fetch_limit(64), 4);
|
|
let gate = std::sync::Arc::new(super::RawTransactionIngestFairTurnGate::new(1));
|
|
let first = super::RawTransactionIngestFairWaiterGuard::register(std::sync::Arc::clone(&gate), super::RawTransactionIngestTrafficClass::Repair);
|
|
let first = match first {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
assert!(super::RawTransactionIngestFairWaiterGuard::register(std::sync::Arc::clone(&gate), super::RawTransactionIngestTrafficClass::Nominal,).is_err());
|
|
std::mem::drop(first);
|
|
assert!(super::RawTransactionIngestFairWaiterGuard::register(gate, super::RawTransactionIngestTrafficClass::Nominal).is_ok());
|
|
return;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn v0_3_14_pre_010_nominal_and_repair_hydration_share_one_capacity_one_permit_pool() {
|
|
let registry = std::sync::Arc::new(super::RawTransactionIngestGlobalHydrationRegistry::new(2, 1));
|
|
let nominal = registry.acquire_hydration_permit(super::RawTransactionIngestTrafficClass::Nominal).await;
|
|
let nominal = match nominal {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let repair_registry = std::sync::Arc::clone(®istry);
|
|
let repair = tokio::spawn(async move {
|
|
return repair_registry.acquire_hydration_permit(super::RawTransactionIngestTrafficClass::Repair).await;
|
|
});
|
|
tokio::task::yield_now().await;
|
|
assert!(!repair.is_finished());
|
|
std::mem::drop(nominal);
|
|
let repair = match repair.await {
|
|
std::result::Result::Ok(std::result::Result::Ok(value)) => value,
|
|
std::result::Result::Ok(std::result::Result::Err(_)) | std::result::Result::Err(_) => return,
|
|
};
|
|
assert_eq!(registry.hydration_permits.available_permits(), 0);
|
|
std::mem::drop(repair);
|
|
assert_eq!(registry.hydration_permits.available_permits(), 1);
|
|
return;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn v0_3_14_pre_010_repair_only_progresses_when_existing_capacity_is_one() {
|
|
let registry = super::RawTransactionIngestGlobalHydrationRegistry::new(1, 1);
|
|
let permit = registry.acquire_hydration_permit(super::RawTransactionIngestTrafficClass::Repair).await;
|
|
let permit = match permit {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
assert_eq!(registry.hydration_permits.available_permits(), 0);
|
|
std::mem::drop(permit);
|
|
assert_eq!(registry.hydration_permits.available_permits(), 1);
|
|
return;
|
|
}
|