754 lines
40 KiB
Rust
754 lines
40 KiB
Rust
// file: crates/ksp-store-postgres-lib/unit_tests/raw_transaction.rs
|
|
// version: 8
|
|
|
|
fn network() -> ksp_store_api::RawNetworkId {
|
|
return match ksp_store_api::RawNetworkId::new("devnet") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("valid test network rejected: {error:?}"),
|
|
};
|
|
}
|
|
|
|
fn transaction_row(state: &str) -> super::RawTransactionDbRow {
|
|
return super::RawTransactionDbRow {
|
|
archive_payload: std::option::Option::None,
|
|
block_time_unix_millis: std::option::Option::Some(1_700_000_000_000),
|
|
content_hash: vec![7; 32],
|
|
format_id: "ksp.raw.transaction".to_owned(),
|
|
format_version: i64::from(u32::MAX),
|
|
payload: std::option::Option::Some(vec![1, 2, 3, 4]),
|
|
retention_state: state.to_owned(),
|
|
signature: vec![9; 64],
|
|
slot_text: u64::MAX.to_string(),
|
|
};
|
|
}
|
|
|
|
#[test]
|
|
fn pre_004_full_and_archived_rows_round_trip_without_integer_narrowing() {
|
|
let network = network();
|
|
let full = super::decode_raw_transaction_row(&network, transaction_row("full"));
|
|
let full = match full {
|
|
std::result::Result::Ok(std::option::Option::Some(value)) => value,
|
|
other => panic!("valid full row rejected: {other:?}"),
|
|
};
|
|
assert_eq!(full.slot(), u64::MAX);
|
|
assert_eq!(full.block_time().map(|value| return value.unix_millis()), std::option::Option::Some(1_700_000_000_000));
|
|
assert_eq!(full.payload().format_version(), u32::MAX);
|
|
assert_eq!(full.payload().bytes(), &[1, 2, 3, 4]);
|
|
assert_eq!(full.reference().network().as_str(), "devnet");
|
|
assert_eq!(full.reference().signature().as_bytes(), &[9; 64]);
|
|
let mut archived_row = transaction_row("archived");
|
|
archived_row.payload = std::option::Option::None;
|
|
archived_row.archive_payload = std::option::Option::Some(vec![5, 6, 7]);
|
|
let archived = super::decode_raw_transaction_row(&network, archived_row);
|
|
let archived = match archived {
|
|
std::result::Result::Ok(std::option::Option::Some(value)) => value,
|
|
other => panic!("valid archived row rejected: {other:?}"),
|
|
};
|
|
assert_eq!(archived.slot(), u64::MAX);
|
|
assert_eq!(archived.payload().bytes(), &[5, 6, 7]);
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn pre_004_purged_get_returns_none_and_rejects_payload_or_block_time_residue() {
|
|
let network = network();
|
|
let mut purged_row = transaction_row("purged");
|
|
purged_row.payload = std::option::Option::None;
|
|
purged_row.archive_payload = std::option::Option::None;
|
|
purged_row.block_time_unix_millis = std::option::Option::None;
|
|
let purged = super::decode_raw_transaction_row(&network, purged_row);
|
|
assert!(matches!(purged, std::result::Result::Ok(std::option::Option::None)));
|
|
let mut malformed = transaction_row("purged");
|
|
malformed.archive_payload = std::option::Option::None;
|
|
malformed.block_time_unix_millis = std::option::Option::None;
|
|
let malformed = super::decode_raw_transaction_row(&network, malformed);
|
|
assert_eq!(malformed.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::DataInvalid));
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn pre_004_observation_row_reconstructs_complete_safe_provenance() {
|
|
let network = network();
|
|
let row = super::RawObservationDbRow {
|
|
acquisition_method: "transactionSubscribe".to_owned(),
|
|
capture_session_id: std::option::Option::Some("capture-1".to_owned()),
|
|
commitment: std::option::Option::Some("confirmed".to_owned()),
|
|
endpoint_id: std::option::Option::Some("publicnode-devnet".to_owned()),
|
|
filter_id: std::option::Option::Some("filter-1".to_owned()),
|
|
observation_key: vec![3; 32],
|
|
observed_at_unix_millis: std::option::Option::Some(1_699_999_999_000),
|
|
origin: "live".to_owned(),
|
|
protocol: "solana-ws".to_owned(),
|
|
provider: "publicnode".to_owned(),
|
|
received_at_unix_millis: 1_700_000_000_000,
|
|
source_payload_hash: std::option::Option::Some(vec![4; 32]),
|
|
source_payload_size_bytes: std::option::Option::Some(64 * 1024 * 1024),
|
|
transaction_signature: vec![8; 64],
|
|
};
|
|
let decoded = super::decode_raw_observation_row(&network, row);
|
|
let decoded = match decoded {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("valid observation row rejected: {error:?}"),
|
|
};
|
|
assert_eq!(decoded.observation_key().as_bytes(), &[3; 32]);
|
|
assert_eq!(decoded.transaction().network().as_str(), "devnet");
|
|
assert_eq!(decoded.transaction().signature().as_bytes(), &[8; 64]);
|
|
assert_eq!(decoded.provenance().provider().as_str(), "publicnode");
|
|
assert_eq!(decoded.provenance().protocol().as_str(), "solana-ws");
|
|
assert_eq!(decoded.provenance().acquisition_method().as_str(), "transactionSubscribe");
|
|
assert_eq!(decoded.provenance().capture_session_id().map(|value| return value.as_str()), std::option::Option::Some("capture-1"));
|
|
assert_eq!(decoded.provenance().commitment().map(|value| return value.as_str()), std::option::Option::Some("confirmed"));
|
|
assert_eq!(decoded.provenance().endpoint_id().map(|value| return value.as_str()), std::option::Option::Some("publicnode-devnet"));
|
|
assert_eq!(decoded.provenance().filter_id().map(|value| return value.as_str()), std::option::Option::Some("filter-1"));
|
|
assert_eq!(decoded.provenance().observed_at().map(|value| return value.unix_millis()), std::option::Option::Some(1_699_999_999_000));
|
|
assert_eq!(decoded.provenance().received_at().unix_millis(), 1_700_000_000_000);
|
|
assert_eq!(decoded.provenance().source_payload_hash().map(|value| return *value.as_bytes()), std::option::Option::Some([4; 32]));
|
|
assert_eq!(decoded.provenance().source_payload_size_bytes(), std::option::Option::Some(64 * 1024 * 1024));
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn pre_004_hostile_rows_map_to_static_data_invalid_without_echoing_values() {
|
|
let network = network();
|
|
let mut oversized_slot = transaction_row("full");
|
|
oversized_slot.slot_text = "18446744073709551616".to_owned();
|
|
let slot_error = super::decode_raw_transaction_row(&network, oversized_slot).err();
|
|
assert_eq!(slot_error.as_ref().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::DataInvalid));
|
|
assert_eq!(slot_error.as_ref().map(|value| return value.phase()), std::option::Option::Some("raw_transaction_slot"));
|
|
let mut bad_hash = transaction_row("full");
|
|
bad_hash.content_hash = vec![1; 31];
|
|
let hash_error = super::decode_raw_transaction_row(&network, bad_hash).err();
|
|
assert_eq!(hash_error.as_ref().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::DataInvalid));
|
|
assert!(!format!("{hash_error:?}").contains("18446744073709551616"));
|
|
let bad_origin = super::decode_origin("https://hostile.invalid/secret");
|
|
let origin_error = bad_origin.err();
|
|
assert_eq!(origin_error.as_ref().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::DataInvalid));
|
|
assert!(!format!("{origin_error:?}").contains("hostile.invalid"));
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn pre_004_retention_and_tombstone_decoding_is_exact() {
|
|
assert!(matches!(super::decode_retention_state("full"), std::result::Result::Ok(ksp_store_api::RawRetentionState::Full)));
|
|
assert!(matches!(super::decode_retention_state("archived"), std::result::Result::Ok(ksp_store_api::RawRetentionState::Archived)));
|
|
assert!(matches!(super::decode_retention_state("purged"), std::result::Result::Ok(ksp_store_api::RawRetentionState::Purged)));
|
|
assert_eq!(
|
|
super::decode_retention_state("compacted").err().map(|value| return value.kind()),
|
|
std::option::Option::Some(crate::PostgresBackendErrorKind::DataInvalid),
|
|
);
|
|
let network = network();
|
|
let row = super::RawTombstoneDbRow {
|
|
block_time_unix_millis: std::option::Option::None,
|
|
content_hash: vec![6; 32],
|
|
format_id: "ksp.raw.transaction".to_owned(),
|
|
format_version: i64::from(u32::MAX),
|
|
retention_state: "purged".to_owned(),
|
|
signature: vec![2; 64],
|
|
slot_text: u64::MAX.to_string(),
|
|
};
|
|
let tombstone = super::decode_raw_tombstone_row(&network, row);
|
|
let tombstone = match tombstone {
|
|
std::result::Result::Ok(std::option::Option::Some(value)) => value,
|
|
other => panic!("valid tombstone row rejected: {other:?}"),
|
|
};
|
|
assert_eq!(tombstone.slot(), u64::MAX);
|
|
assert_eq!(tombstone.format_version(), u32::MAX);
|
|
assert_eq!(tombstone.content_hash().as_bytes(), &[6; 32]);
|
|
assert_eq!(tombstone.reference().signature().as_bytes(), &[2; 64]);
|
|
let malformed = super::RawTombstoneDbRow {
|
|
block_time_unix_millis: std::option::Option::Some(1),
|
|
content_hash: vec![6; 32],
|
|
format_id: "ksp.raw.transaction".to_owned(),
|
|
format_version: 1,
|
|
retention_state: "purged".to_owned(),
|
|
signature: vec![2; 64],
|
|
slot_text: "1".to_owned(),
|
|
};
|
|
let malformed = super::decode_raw_tombstone_row(&network, malformed);
|
|
assert_eq!(malformed.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::DataInvalid));
|
|
return;
|
|
}
|
|
|
|
fn inspection_row(
|
|
retention_state: &str,
|
|
hot_payload_present: bool,
|
|
archive_payload_present: bool,
|
|
payload_size_bytes: std::option::Option<i64>,
|
|
block_time_unix_millis: std::option::Option<i64>,
|
|
) -> super::RawInspectionDbRow {
|
|
return super::RawInspectionDbRow {
|
|
archive_payload_present: std::option::Option::Some(archive_payload_present),
|
|
block_time_unix_millis,
|
|
content_hash: std::option::Option::Some(vec![7; 32]),
|
|
filtered_count_text: "3".to_owned(),
|
|
format_id: std::option::Option::Some("ksp.raw.transaction".to_owned()),
|
|
format_version: std::option::Option::Some(1),
|
|
hot_payload_present: std::option::Option::Some(hot_payload_present),
|
|
page_present: true,
|
|
payload_size_bytes,
|
|
retention_state: std::option::Option::Some(retention_state.to_owned()),
|
|
signature: std::option::Option::Some(vec![9; 64]),
|
|
slot_text: std::option::Option::Some(u64::MAX.to_string()),
|
|
total_count_text: "4".to_owned(),
|
|
};
|
|
}
|
|
|
|
#[test]
|
|
fn v0_3_8_pre_004_inspection_window_rejects_unrepresentable_limit_and_offset_before_io() {
|
|
let normal_limit = match ksp_store_api::RawPageLimit::new(100) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("valid inspection page limit rejected: {error:?}"),
|
|
};
|
|
let normal = super::raw_transaction_inspection_sql_window(ksp_store_api::RawInspectionPageRequest::new(25, normal_limit));
|
|
assert_eq!(normal, std::result::Result::Ok((100, 25)));
|
|
let huge_limit = match ksp_store_api::RawPageLimit::new(u64::MAX) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("API unexpectedly rejected backend-physical limit fixture: {error:?}"),
|
|
};
|
|
let limit_error = super::raw_transaction_inspection_sql_window(ksp_store_api::RawInspectionPageRequest::new(0, huge_limit));
|
|
assert_eq!(
|
|
limit_error.err().map(|value| return (value.kind(), value.phase())),
|
|
std::option::Option::Some((crate::PostgresBackendErrorKind::PageLimitUnsupported, "raw_transaction_inspection_limit")),
|
|
);
|
|
let offset_error = super::raw_transaction_inspection_sql_window(ksp_store_api::RawInspectionPageRequest::new(u64::MAX, normal_limit));
|
|
assert_eq!(
|
|
offset_error.err().map(|value| return (value.kind(), value.phase())),
|
|
std::option::Option::Some((crate::PostgresBackendErrorKind::QueryInvalid, "raw_transaction_inspection_offset")),
|
|
);
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn v0_3_8_pre_004_inspection_summary_decoding_handles_full_archived_and_purged_without_payload_bytes() {
|
|
let network = network();
|
|
let full = super::decode_raw_inspection_summary(
|
|
&network,
|
|
inspection_row("full", true, false, std::option::Option::Some(4), std::option::Option::Some(1_700_000_000_000)),
|
|
);
|
|
let full = match full {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("valid full inspection summary rejected: {error:?}"),
|
|
};
|
|
assert_eq!(full.slot(), u64::MAX);
|
|
assert_eq!(full.payload_size_bytes(), std::option::Option::Some(4));
|
|
assert_eq!(full.retention_state(), ksp_store_api::RawRetentionState::Full);
|
|
let archived = super::decode_raw_inspection_summary(
|
|
&network,
|
|
inspection_row("archived", false, true, std::option::Option::Some(5), std::option::Option::Some(1_700_000_000_000)),
|
|
);
|
|
let archived = match archived {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("valid archived inspection summary rejected: {error:?}"),
|
|
};
|
|
assert_eq!(archived.payload_size_bytes(), std::option::Option::Some(5));
|
|
assert_eq!(archived.retention_state(), ksp_store_api::RawRetentionState::Archived);
|
|
let purged = super::decode_raw_inspection_summary(&network, inspection_row("purged", false, false, std::option::Option::None, std::option::Option::None));
|
|
let purged = match purged {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("valid purged inspection summary rejected: {error:?}"),
|
|
};
|
|
assert_eq!(purged.payload_size_bytes(), std::option::Option::None);
|
|
assert_eq!(purged.block_time(), std::option::Option::None);
|
|
assert_eq!(purged.retention_state(), ksp_store_api::RawRetentionState::Purged);
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn v0_3_8_pre_004_inspection_summary_rejects_physical_retention_shape_residue() {
|
|
let network = network();
|
|
for malformed in [
|
|
inspection_row("full", true, true, std::option::Option::Some(4), std::option::Option::Some(1)),
|
|
inspection_row("archived", true, true, std::option::Option::Some(4), std::option::Option::Some(1)),
|
|
inspection_row("purged", false, true, std::option::Option::None, std::option::Option::None),
|
|
inspection_row("purged", false, false, std::option::Option::None, std::option::Option::Some(1)),
|
|
] {
|
|
let result = super::decode_raw_inspection_summary(&network, malformed);
|
|
assert_eq!(result.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::DataInvalid));
|
|
}
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn pre_004_wrong_network_is_rejected_by_the_private_pre_io_guard() {
|
|
let backend_network = network();
|
|
let other_network = match ksp_store_api::RawNetworkId::new("mainnet") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("valid alternate network rejected: {error:?}"),
|
|
};
|
|
let reference = ksp_store_api::RawTransactionReference::new(other_network, ksp_store_api::RawTransactionSignature::new([1; 64]));
|
|
let rejected = super::ensure_network(&backend_network, &reference, "test_network");
|
|
assert_eq!(rejected.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::WrongNetwork));
|
|
return;
|
|
}
|
|
|
|
fn raw_transaction(signature_byte: u8, payload_bytes: &[u8], content_hash_byte: u8) -> ksp_store_api::RawTransaction {
|
|
let reference = ksp_store_api::RawTransactionReference::new(network(), ksp_store_api::RawTransactionSignature::new([signature_byte; 64]));
|
|
let format_id = match ksp_store_api::RawFormatId::new("ksp.raw.transaction") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("valid test format rejected: {error:?}"),
|
|
};
|
|
let payload = match ksp_store_api::RawPayload::try_new(
|
|
format_id,
|
|
1,
|
|
payload_bytes.to_vec().into_boxed_slice(),
|
|
ksp_store_api::RawContentHash::new([content_hash_byte; 32]),
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("valid test payload rejected: {error:?}"),
|
|
};
|
|
return ksp_store_api::RawTransaction::new(reference, 42, std::option::Option::None, payload);
|
|
}
|
|
|
|
fn observation(reference: ksp_store_api::RawTransactionReference, key_byte: u8, provider: &str) -> ksp_store_api::RawTransactionObservation {
|
|
let provider = match ksp_store_api::RawProvenanceCode::new(provider) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("valid test provider rejected: {error:?}"),
|
|
};
|
|
let protocol = match ksp_store_api::RawProvenanceCode::new("solana-ws") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("valid test protocol rejected: {error:?}"),
|
|
};
|
|
let method = match ksp_store_api::RawProvenanceCode::new("transactionSubscribe") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("valid test method rejected: {error:?}"),
|
|
};
|
|
let received_at = match ksp_store_api::RawTimestamp::from_unix_millis(1_700_000_000_000) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("valid test timestamp rejected: {error:?}"),
|
|
};
|
|
let provenance = ksp_store_api::RawAcquisitionProvenance::new(provider, protocol, method, ksp_store_api::RawAcquisitionOrigin::Live, received_at);
|
|
return ksp_store_api::RawTransactionObservation::new(ksp_store_api::RawObservationKey::new([key_byte; 32]), reference, provenance);
|
|
}
|
|
|
|
#[test]
|
|
fn pre_005_atomic_acquisition_pre_io_guard_requires_backend_network_and_exact_reference() {
|
|
let backend_network = network();
|
|
let raw_transaction = raw_transaction(1, &[1, 2, 3], 7);
|
|
let matching = observation(raw_transaction.reference().clone(), 3, "publicnode");
|
|
assert!(super::ensure_acquisition_inputs(&backend_network, &raw_transaction, &matching).is_ok());
|
|
let other_reference = ksp_store_api::RawTransactionReference::new(backend_network.clone(), ksp_store_api::RawTransactionSignature::new([2; 64]));
|
|
let mismatched = observation(other_reference, 4, "publicnode");
|
|
let mismatch = super::ensure_acquisition_inputs(&backend_network, &raw_transaction, &mismatched);
|
|
assert_eq!(mismatch.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::Conflict));
|
|
let other_network = match ksp_store_api::RawNetworkId::new("mainnet") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("valid alternate network rejected: {error:?}"),
|
|
};
|
|
let foreign_reference = ksp_store_api::RawTransactionReference::new(other_network, ksp_store_api::RawTransactionSignature::new([1; 64]));
|
|
let foreign = observation(foreign_reference, 5, "publicnode");
|
|
let wrong_network = super::ensure_acquisition_inputs(&backend_network, &raw_transaction, &foreign);
|
|
assert_eq!(wrong_network.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::WrongNetwork));
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn pre_005_existing_full_content_requires_exact_payload_equality_not_hash_only() {
|
|
let network = network();
|
|
let incoming = raw_transaction(9, &[1, 2, 3, 4], 7);
|
|
let matching = super::compare_existing_transaction(&network, transaction_row("full"), &incoming);
|
|
assert!(matching.is_err(), "fixture intentionally differs in slot/content and must conflict");
|
|
let mut exact_row = transaction_row("full");
|
|
exact_row.signature = vec![9; 64];
|
|
exact_row.slot_text = "42".to_owned();
|
|
exact_row.block_time_unix_millis = std::option::Option::None;
|
|
exact_row.format_version = 1;
|
|
exact_row.payload = std::option::Option::Some(vec![1, 2, 3, 4]);
|
|
let exact = super::compare_existing_transaction(&network, exact_row, &incoming);
|
|
assert!(matches!(exact, std::result::Result::Ok(super::ExistingTransactionMatch::Active)));
|
|
let mut same_hash_different_bytes = transaction_row("full");
|
|
same_hash_different_bytes.signature = vec![9; 64];
|
|
same_hash_different_bytes.slot_text = "42".to_owned();
|
|
same_hash_different_bytes.block_time_unix_millis = std::option::Option::None;
|
|
same_hash_different_bytes.format_version = 1;
|
|
same_hash_different_bytes.payload = std::option::Option::Some(vec![9, 9, 9, 9]);
|
|
let conflict = super::compare_existing_transaction(&network, same_hash_different_bytes, &incoming);
|
|
assert_eq!(conflict.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::Conflict));
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn pre_005_purged_tombstone_matches_only_retained_identity_metadata() {
|
|
let network = network();
|
|
let incoming = raw_transaction(9, &[1, 2, 3, 4], 7);
|
|
let mut purged = transaction_row("purged");
|
|
purged.signature = vec![9; 64];
|
|
purged.slot_text = "42".to_owned();
|
|
purged.block_time_unix_millis = std::option::Option::None;
|
|
purged.format_version = 1;
|
|
purged.payload = std::option::Option::None;
|
|
purged.archive_payload = std::option::Option::None;
|
|
let compatible = super::compare_existing_transaction(&network, purged, &incoming);
|
|
assert!(matches!(compatible, std::result::Result::Ok(super::ExistingTransactionMatch::Purged)));
|
|
let mut divergent = transaction_row("purged");
|
|
divergent.signature = vec![9; 64];
|
|
divergent.slot_text = "42".to_owned();
|
|
divergent.block_time_unix_millis = std::option::Option::None;
|
|
divergent.format_version = 1;
|
|
divergent.content_hash = vec![8; 32];
|
|
divergent.payload = std::option::Option::None;
|
|
divergent.archive_payload = std::option::Option::None;
|
|
let conflict = super::compare_existing_transaction(&network, divergent, &incoming);
|
|
assert_eq!(conflict.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::Conflict));
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn v0_3_15_pre_012_fix_004_content_conflict_diagnostic_classifies_raw_v1_components_without_payload_values() {
|
|
let stored = raw_transaction(
|
|
9,
|
|
br#"{"transaction":["AQID","base64"],"meta":{"err":null,"fee":5000,"logMessages":["left"],"rewards":[]},"version":0,"transactionIndex":7}"#,
|
|
7,
|
|
);
|
|
let incoming = raw_transaction(9, br#"{"transaction":["AQID","base64"],"meta":{"err":null,"fee":5000,"logMessages":["right"],"rewards":[{"lamports":1}]},"version":0,"transactionIndex":8}"#, 8);
|
|
let diagnostic = super::raw_transaction_content_conflict_diagnostic(&stored, &incoming);
|
|
assert!(!diagnostic.slot_mismatch);
|
|
assert!(!diagnostic.block_time_mismatch);
|
|
assert!(!diagnostic.format_id_mismatch);
|
|
assert!(!diagnostic.format_version_mismatch);
|
|
assert!(diagnostic.content_hash_mismatch);
|
|
assert!(diagnostic.payload_bytes_mismatch);
|
|
assert!(diagnostic.payload_diagnostic_available);
|
|
assert!(!diagnostic.payload_transaction_mismatch);
|
|
assert!(diagnostic.payload_meta_mismatch);
|
|
assert!(!diagnostic.payload_version_mismatch);
|
|
assert!(diagnostic.payload_transaction_index_mismatch);
|
|
assert!(!diagnostic.payload_other_mismatch);
|
|
assert_eq!(diagnostic.meta_mismatch_fields, "logMessages,rewards");
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn v0_3_15_pre_012_fix_004_content_conflict_diagnostic_collapses_unknown_meta_keys_to_other() {
|
|
let stored = raw_transaction(9, br#"{"transaction":["AQID","base64"],"meta":{"err":null,"futureField":1},"version":0}"#, 7);
|
|
let incoming = raw_transaction(9, br#"{"transaction":["BAUG","base64"],"meta":{"err":null,"futureField":2},"version":1}"#, 8);
|
|
let diagnostic = super::raw_transaction_content_conflict_diagnostic(&stored, &incoming);
|
|
assert!(diagnostic.payload_transaction_mismatch);
|
|
assert!(diagnostic.payload_meta_mismatch);
|
|
assert!(diagnostic.payload_version_mismatch);
|
|
assert_eq!(diagnostic.meta_mismatch_fields, "other");
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn v0_3_15_pre_014_log_messages_conflict_diagnostic_distinguishes_prefix_truncation_and_line_kind_without_text() {
|
|
let stored = raw_transaction(
|
|
9,
|
|
br#"{"transaction":["AQID","base64"],"meta":{"logMessages":["Program Vote111 invoke [1]","Program log: stable","Program Vote111 success"]},"version":0}"#,
|
|
7,
|
|
);
|
|
let incoming = raw_transaction(
|
|
9,
|
|
br#"{"transaction":["AQID","base64"],"meta":{"logMessages":["Program Vote111 invoke [1]","Program log: stable","Log truncated"]},"version":0}"#,
|
|
8,
|
|
);
|
|
let diagnostic = super::raw_transaction_content_conflict_diagnostic(&stored, &incoming);
|
|
assert!(diagnostic.log_messages.available);
|
|
assert_eq!(diagnostic.log_messages.stored_state, "array");
|
|
assert_eq!(diagnostic.log_messages.incoming_state, "array");
|
|
assert_eq!(diagnostic.log_messages.stored_count, 3);
|
|
assert_eq!(diagnostic.log_messages.incoming_count, 3);
|
|
assert_eq!(diagnostic.log_messages.first_mismatch_index, "2");
|
|
assert_eq!(diagnostic.log_messages.stored_first_kind, "program_success");
|
|
assert_eq!(diagnostic.log_messages.incoming_first_kind, "log_truncated");
|
|
assert!(!diagnostic.log_messages.stored_has_truncation_marker);
|
|
assert!(diagnostic.log_messages.incoming_has_truncation_marker);
|
|
assert!(!diagnostic.log_messages.stored_prefix_of_incoming);
|
|
assert!(!diagnostic.log_messages.incoming_prefix_of_stored);
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn v0_3_15_pre_014_log_messages_conflict_diagnostic_detects_strict_prefix_without_exposing_line_material() {
|
|
let stored = raw_transaction(
|
|
9,
|
|
br#"{"transaction":["AQID","base64"],"meta":{"logMessages":["Program Vote111 invoke [1]","Program Vote111 success"]},"version":0}"#,
|
|
7,
|
|
);
|
|
let incoming = raw_transaction(9, br#"{"transaction":["AQID","base64"],"meta":{"logMessages":["Program Vote111 invoke [1]"]},"version":0}"#, 8);
|
|
let diagnostic = super::raw_transaction_content_conflict_diagnostic(&stored, &incoming);
|
|
assert!(diagnostic.log_messages.available);
|
|
assert_eq!(diagnostic.log_messages.first_mismatch_index, "1");
|
|
assert_eq!(diagnostic.log_messages.stored_first_kind, "program_success");
|
|
assert_eq!(diagnostic.log_messages.incoming_first_kind, "missing");
|
|
assert!(!diagnostic.log_messages.stored_prefix_of_incoming);
|
|
assert!(diagnostic.log_messages.incoming_prefix_of_stored);
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn pre_005_observation_idempotence_compares_reference_and_complete_provenance() {
|
|
let incoming_transaction = raw_transaction(8, &[1, 2, 3], 4);
|
|
let incoming = observation(incoming_transaction.reference().clone(), 3, "publicnode");
|
|
let matching_row = super::RawObservationDbRow {
|
|
acquisition_method: "transactionSubscribe".to_owned(),
|
|
capture_session_id: std::option::Option::None,
|
|
commitment: std::option::Option::None,
|
|
endpoint_id: std::option::Option::None,
|
|
filter_id: std::option::Option::None,
|
|
observation_key: vec![3; 32],
|
|
observed_at_unix_millis: std::option::Option::None,
|
|
origin: "live".to_owned(),
|
|
protocol: "solana-ws".to_owned(),
|
|
provider: "publicnode".to_owned(),
|
|
received_at_unix_millis: 1_700_000_000_000,
|
|
source_payload_hash: std::option::Option::None,
|
|
source_payload_size_bytes: std::option::Option::None,
|
|
transaction_signature: vec![8; 64],
|
|
};
|
|
let matching = super::decode_raw_observation_row(&network(), matching_row);
|
|
let matching = match matching {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("valid matching observation rejected: {error:?}"),
|
|
};
|
|
assert!(matching.eq(&incoming));
|
|
let divergent_row = super::RawObservationDbRow {
|
|
acquisition_method: "transactionSubscribe".to_owned(),
|
|
capture_session_id: std::option::Option::None,
|
|
commitment: std::option::Option::None,
|
|
endpoint_id: std::option::Option::None,
|
|
filter_id: std::option::Option::None,
|
|
observation_key: vec![3; 32],
|
|
observed_at_unix_millis: std::option::Option::None,
|
|
origin: "live".to_owned(),
|
|
protocol: "solana-ws".to_owned(),
|
|
provider: "another-provider".to_owned(),
|
|
received_at_unix_millis: 1_700_000_000_000,
|
|
source_payload_hash: std::option::Option::None,
|
|
source_payload_size_bytes: std::option::Option::None,
|
|
transaction_signature: vec![8; 64],
|
|
};
|
|
let divergent = super::decode_raw_observation_row(&network(), divergent_row);
|
|
let divergent = match divergent {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("valid divergent observation rejected: {error:?}"),
|
|
};
|
|
assert!(!divergent.eq(&incoming));
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn pre_005_observation_origin_encoding_is_exact_and_static() {
|
|
assert_eq!(super::encode_origin(ksp_store_api::RawAcquisitionOrigin::Backfill), std::result::Result::Ok("backfill"));
|
|
assert_eq!(super::encode_origin(ksp_store_api::RawAcquisitionOrigin::Import), std::result::Result::Ok("import"));
|
|
assert_eq!(super::encode_origin(ksp_store_api::RawAcquisitionOrigin::Live), std::result::Result::Ok("live"));
|
|
assert_eq!(super::encode_origin(ksp_store_api::RawAcquisitionOrigin::Repair), std::result::Result::Ok("repair"));
|
|
assert_eq!(super::encode_origin(ksp_store_api::RawAcquisitionOrigin::Replay), std::result::Result::Ok("replay"));
|
|
return;
|
|
}
|
|
|
|
fn page_query(
|
|
network_id: &str,
|
|
start: std::option::Option<u64>,
|
|
end: std::option::Option<u64>,
|
|
direction: ksp_store_api::RawSortDirection,
|
|
cursor: std::option::Option<ksp_store_api::RawPageCursor>,
|
|
) -> ksp_store_api::RawTransactionQuery {
|
|
let network = match ksp_store_api::RawNetworkId::new(network_id) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("valid page-query network rejected: {error:?}"),
|
|
};
|
|
let range = match ksp_store_api::RawSlotRange::new(start, end) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("valid page-query range rejected: {error:?}"),
|
|
};
|
|
let limit = match ksp_store_api::RawPageLimit::new(2) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("valid page limit rejected: {error:?}"),
|
|
};
|
|
let page = match cursor {
|
|
std::option::Option::Some(value) => ksp_store_api::RawPageRequest::after(limit, value),
|
|
std::option::Option::None => ksp_store_api::RawPageRequest::first(limit),
|
|
};
|
|
return ksp_store_api::RawTransactionQuery::new(network, range, direction, page);
|
|
}
|
|
|
|
#[test]
|
|
fn pre_006_cursor_v1_round_trip_is_fixed_109_bytes_and_exact() {
|
|
let query = page_query("devnet", std::option::Option::Some(10), std::option::Option::Some(20), ksp_store_api::RawSortDirection::Ascending, None);
|
|
let signature = ksp_store_api::RawTransactionSignature::new([7_u8; 64]);
|
|
let cursor = match crate::encode_raw_transaction_cursor(&query, 15, &signature) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("valid cursor encoding rejected: {error:?}"),
|
|
};
|
|
assert_eq!(cursor.as_bytes().len(), 109);
|
|
assert_eq!(cursor.as_bytes().get(0..4), std::option::Option::Some(b"KSPT".as_ref()));
|
|
assert_eq!(cursor.as_bytes().get(4).copied(), std::option::Option::Some(1));
|
|
let decoded = match crate::decode_raw_transaction_cursor(&query, &cursor) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("valid cursor decoding rejected: {error:?}"),
|
|
};
|
|
assert_eq!(decoded.last_slot, 15);
|
|
assert_eq!(decoded.last_signature, [7_u8; 64]);
|
|
assert_eq!(std::format!("{cursor:?}"), "RawPageCursor { len: 109 }");
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn pre_006_cursor_v1_rejects_replay_across_network_direction_and_range() {
|
|
let query = page_query("devnet", std::option::Option::Some(10), std::option::Option::Some(20), ksp_store_api::RawSortDirection::Ascending, None);
|
|
let signature = ksp_store_api::RawTransactionSignature::new([9_u8; 64]);
|
|
let cursor = match crate::encode_raw_transaction_cursor(&query, 15, &signature) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("valid cursor encoding rejected: {error:?}"),
|
|
};
|
|
let other_network = page_query("mainnet", Some(10), Some(20), ksp_store_api::RawSortDirection::Ascending, None);
|
|
let other_direction = page_query("devnet", Some(10), Some(20), ksp_store_api::RawSortDirection::Descending, None);
|
|
let other_range = page_query("devnet", Some(11), Some(20), ksp_store_api::RawSortDirection::Ascending, None);
|
|
for candidate in [&other_network, &other_direction, &other_range] {
|
|
let error = crate::decode_raw_transaction_cursor(candidate, &cursor).err();
|
|
assert_eq!(error.map(|value| return value.kind()), Some(crate::PostgresBackendErrorKind::QueryInvalid));
|
|
}
|
|
let outside_range = page_query("devnet", Some(16), Some(20), ksp_store_api::RawSortDirection::Ascending, None);
|
|
let error = crate::decode_raw_transaction_cursor(&outside_range, &cursor).err();
|
|
assert_eq!(error.map(|value| return value.phase()), Some("raw_cursor_slot_range"));
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn pre_006_cursor_v1_hostile_size_magic_version_and_digest_are_rejected() {
|
|
let query = page_query("devnet", None, None, ksp_store_api::RawSortDirection::Ascending, None);
|
|
for size in [1_usize, 108, 109, 110, 4_096] {
|
|
let raw = match ksp_store_api::RawPageCursor::try_new(vec![0_u8; size].into_boxed_slice()) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("bounded hostile cursor rejected by API before backend test: {error:?}"),
|
|
};
|
|
assert!(crate::decode_raw_transaction_cursor(&query, &raw).is_err());
|
|
}
|
|
let signature = ksp_store_api::RawTransactionSignature::new([3_u8; 64]);
|
|
let valid = match crate::encode_raw_transaction_cursor(&query, 42, &signature) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("valid cursor encoding rejected: {error:?}"),
|
|
};
|
|
for index in [0_usize, 4, 108] {
|
|
let mut bytes = valid.as_bytes().to_vec();
|
|
bytes[index] ^= 0xff;
|
|
let hostile = match ksp_store_api::RawPageCursor::try_new(bytes.into_boxed_slice()) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("fixed-size hostile cursor rejected by API before backend test: {error:?}"),
|
|
};
|
|
let error = crate::decode_raw_transaction_cursor(&query, &hostile).err();
|
|
assert_eq!(error.map(|value| return value.kind()), Some(crate::PostgresBackendErrorKind::QueryInvalid));
|
|
}
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn pre_006_page_limit_exposes_only_the_real_postgres_limit_plus_one_boundary() {
|
|
let maximum = crate::raw_transaction_physical_page_limit(9_223_372_036_854_775_806);
|
|
assert_eq!(maximum, Ok((9_223_372_036_854_775_806_usize, i64::MAX)));
|
|
let rejected = crate::raw_transaction_physical_page_limit(9_223_372_036_854_775_807).err();
|
|
assert_eq!(rejected.map(|value| return value.kind()), Some(crate::PostgresBackendErrorKind::PageLimitUnsupported));
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn pre_007_retention_transition_decision_matches_compare_and_transition_contract() {
|
|
assert_eq!(
|
|
super::retention_transition_decision(
|
|
ksp_store_api::RawRetentionState::Full,
|
|
ksp_store_api::RawRetentionState::Full,
|
|
ksp_store_api::RawRetentionState::Archived,
|
|
),
|
|
std::result::Result::Ok(super::RetentionTransitionDecision::Archive),
|
|
);
|
|
assert_eq!(
|
|
super::retention_transition_decision(
|
|
ksp_store_api::RawRetentionState::Archived,
|
|
ksp_store_api::RawRetentionState::Full,
|
|
ksp_store_api::RawRetentionState::Archived,
|
|
),
|
|
std::result::Result::Ok(super::RetentionTransitionDecision::AlreadyAtTarget),
|
|
);
|
|
assert_eq!(
|
|
super::retention_transition_decision(
|
|
ksp_store_api::RawRetentionState::Archived,
|
|
ksp_store_api::RawRetentionState::Archived,
|
|
ksp_store_api::RawRetentionState::Purged,
|
|
),
|
|
std::result::Result::Ok(super::RetentionTransitionDecision::Purge),
|
|
);
|
|
assert_eq!(
|
|
super::retention_transition_decision(
|
|
ksp_store_api::RawRetentionState::Purged,
|
|
ksp_store_api::RawRetentionState::Archived,
|
|
ksp_store_api::RawRetentionState::Purged,
|
|
),
|
|
std::result::Result::Ok(super::RetentionTransitionDecision::AlreadyAtTarget),
|
|
);
|
|
assert_eq!(
|
|
super::retention_transition_decision(
|
|
ksp_store_api::RawRetentionState::Full,
|
|
ksp_store_api::RawRetentionState::Archived,
|
|
ksp_store_api::RawRetentionState::Purged,
|
|
),
|
|
std::result::Result::Ok(super::RetentionTransitionDecision::ExpectedStateMismatch),
|
|
);
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn pre_007_compacted_transitions_are_rejected_by_the_pre_io_guard() {
|
|
let backend_network = network();
|
|
let reference = ksp_store_api::RawTransactionReference::new(backend_network.clone(), ksp_store_api::RawTransactionSignature::new([5; 64]));
|
|
let compact = match ksp_store_api::RawTransactionRetentionTransition::try_new(
|
|
reference.clone(),
|
|
ksp_store_api::RawRetentionState::Full,
|
|
ksp_store_api::RawRetentionState::Compacted,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("valid logical compact transition rejected by API: {error:?}"),
|
|
};
|
|
let compact_error = super::ensure_retention_transition_inputs(&backend_network, &compact).err();
|
|
assert_eq!(
|
|
compact_error.as_ref().map(|value| return value.kind()),
|
|
std::option::Option::Some(crate::PostgresBackendErrorKind::RetentionCompactionUnsupported),
|
|
);
|
|
assert_eq!(compact_error.as_ref().map(|value| return value.phase()), std::option::Option::Some("raw_retention_compaction"));
|
|
let compact_to_archive = match ksp_store_api::RawTransactionRetentionTransition::try_new(
|
|
reference,
|
|
ksp_store_api::RawRetentionState::Compacted,
|
|
ksp_store_api::RawRetentionState::Archived,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("valid logical compacted archive transition rejected by API: {error:?}"),
|
|
};
|
|
let compact_to_archive_error = super::ensure_retention_transition_inputs(&backend_network, &compact_to_archive).err();
|
|
assert_eq!(
|
|
compact_to_archive_error.as_ref().map(|value| return value.kind()),
|
|
std::option::Option::Some(crate::PostgresBackendErrorKind::RetentionCompactionUnsupported),
|
|
);
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn pre_007_retention_transition_pre_io_guard_rejects_wrong_network() {
|
|
let backend_network = network();
|
|
let other_network = match ksp_store_api::RawNetworkId::new("mainnet") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("valid alternate network rejected: {error:?}"),
|
|
};
|
|
let reference = ksp_store_api::RawTransactionReference::new(other_network, ksp_store_api::RawTransactionSignature::new([6; 64]));
|
|
let transition = match ksp_store_api::RawTransactionRetentionTransition::try_new(
|
|
reference,
|
|
ksp_store_api::RawRetentionState::Full,
|
|
ksp_store_api::RawRetentionState::Archived,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("valid retention transition rejected by API: {error:?}"),
|
|
};
|
|
let rejected = super::ensure_retention_transition_inputs(&backend_network, &transition);
|
|
assert_eq!(rejected.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::WrongNetwork));
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn pre_007_retained_payload_shape_rejects_empty_and_oversized_bytes() {
|
|
assert!(super::validate_retained_payload_bytes(&[1], "test_retention_payload").is_ok());
|
|
let empty = super::validate_retained_payload_bytes(&[], "test_retention_payload");
|
|
assert_eq!(empty.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::DataInvalid));
|
|
let oversized = vec![0; ksp_store_api::MAX_RAW_PAYLOAD_BYTES + 1];
|
|
let oversized = super::validate_retained_payload_bytes(oversized.as_slice(), "test_retention_payload");
|
|
assert_eq!(oversized.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::DataInvalid));
|
|
return;
|
|
}
|