0.3.15-pre.014-fix.001
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-store-postgres-lib/src/raw_transaction.rs
|
||||
// version: 9
|
||||
// version: 10
|
||||
|
||||
pub(crate) mod cursor;
|
||||
|
||||
@@ -691,7 +691,7 @@ pub(crate) async fn persist_raw_transaction_acquisition(
|
||||
},
|
||||
};
|
||||
match comparison {
|
||||
ExistingTransactionMatch::Active => ksp_store_api::RawEntityWriteOutcome::AlreadyPresent,
|
||||
ExistingTransactionMatch::Active | ExistingTransactionMatch::ActiveIncomingTruncatedLogs => ksp_store_api::RawEntityWriteOutcome::AlreadyPresent,
|
||||
ExistingTransactionMatch::Purged => {
|
||||
if mode == ksp_store_api::RawTransactionAcquisitionMode::ForceRehydrate {
|
||||
let rehydrate_result = rehydrate_transaction(&sql_transaction, &raw_transaction).await;
|
||||
@@ -853,6 +853,7 @@ pub(crate) async fn transition_raw_transaction_retention(
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum ExistingTransactionMatch {
|
||||
Active,
|
||||
ActiveIncomingTruncatedLogs,
|
||||
Purged,
|
||||
}
|
||||
|
||||
@@ -1143,10 +1144,86 @@ fn compare_existing_transaction(
|
||||
if raw_transactions_equal(&stored, incoming) {
|
||||
return std::result::Result::Ok(ExistingTransactionMatch::Active);
|
||||
}
|
||||
if raw_transaction_incoming_truncated_log_messages_compatible(&stored, incoming) {
|
||||
log_raw_transaction_compatible_truncated_log_messages(network, &stored, incoming);
|
||||
return std::result::Result::Ok(ExistingTransactionMatch::ActiveIncomingTruncatedLogs);
|
||||
}
|
||||
log_raw_transaction_content_conflict(network, &stored, incoming);
|
||||
return std::result::Result::Err(conflict("raw_acquisition_content_conflict"));
|
||||
}
|
||||
|
||||
fn raw_transaction_incoming_truncated_log_messages_compatible(stored: &ksp_store_api::RawTransaction, incoming: &ksp_store_api::RawTransaction) -> bool {
|
||||
if stored.reference() != incoming.reference()
|
||||
|| stored.slot() != incoming.slot()
|
||||
|| stored.block_time() != incoming.block_time()
|
||||
|| stored.payload().format_id() != incoming.payload().format_id()
|
||||
|| stored.payload().format_version() != incoming.payload().format_version()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let stored_payload = serde_json::from_slice::<serde_json::Value>(stored.payload().bytes());
|
||||
let incoming_payload = serde_json::from_slice::<serde_json::Value>(incoming.payload().bytes());
|
||||
let (stored_payload, incoming_payload) = match (stored_payload, incoming_payload) {
|
||||
(std::result::Result::Ok(serde_json::Value::Object(stored)), std::result::Result::Ok(serde_json::Value::Object(incoming))) => (stored, incoming),
|
||||
_ => return false,
|
||||
};
|
||||
if stored_payload.get("transaction") != incoming_payload.get("transaction")
|
||||
|| stored_payload.get("version") != incoming_payload.get("version")
|
||||
|| stored_payload.get("transactionIndex") != incoming_payload.get("transactionIndex")
|
||||
|| json_object_other_fields_mismatch(&stored_payload, &incoming_payload, RAW_TRANSACTION_CONTENT_CONFLICT_PAYLOAD_FIELDS.as_slice())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let (stored_meta, incoming_meta) = match (stored_payload.get("meta"), incoming_payload.get("meta")) {
|
||||
(std::option::Option::Some(serde_json::Value::Object(stored)), std::option::Option::Some(serde_json::Value::Object(incoming))) => (stored, incoming),
|
||||
_ => return false,
|
||||
};
|
||||
if raw_meta_other_than_log_messages_mismatch(stored_meta, incoming_meta) {
|
||||
return false;
|
||||
}
|
||||
let (stored_logs, incoming_logs) = match (stored_meta.get("logMessages"), incoming_meta.get("logMessages")) {
|
||||
(std::option::Option::Some(serde_json::Value::Array(stored)), std::option::Option::Some(serde_json::Value::Array(incoming))) => {
|
||||
(stored.as_slice(), incoming.as_slice())
|
||||
},
|
||||
_ => return false,
|
||||
};
|
||||
return raw_log_messages_incoming_truncated_compatible(stored_logs, incoming_logs);
|
||||
}
|
||||
|
||||
fn raw_meta_other_than_log_messages_mismatch(
|
||||
stored: &serde_json::Map<std::string::String, serde_json::Value>,
|
||||
incoming: &serde_json::Map<std::string::String, serde_json::Value>,
|
||||
) -> bool {
|
||||
let stored_without_logs = stored.iter().filter(|(key, _)| return key.as_str() != "logMessages").collect::<std::collections::BTreeMap<_, _>>();
|
||||
let incoming_without_logs = incoming.iter().filter(|(key, _)| return key.as_str() != "logMessages").collect::<std::collections::BTreeMap<_, _>>();
|
||||
return stored_without_logs != incoming_without_logs;
|
||||
}
|
||||
|
||||
fn raw_log_messages_incoming_truncated_compatible(stored: &[serde_json::Value], incoming: &[serde_json::Value]) -> bool {
|
||||
if raw_log_messages_exact_truncation_marker_count(stored) != 0 {
|
||||
return false;
|
||||
}
|
||||
let marker_index = match incoming.iter().position(raw_log_message_is_exact_truncation_marker) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return false,
|
||||
};
|
||||
if raw_log_messages_exact_truncation_marker_count(incoming) != 1 || marker_index >= stored.len() {
|
||||
return false;
|
||||
}
|
||||
if incoming[..marker_index] != stored[..marker_index] {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
fn raw_log_messages_exact_truncation_marker_count(values: &[serde_json::Value]) -> usize {
|
||||
return values.iter().filter(|value| return raw_log_message_is_exact_truncation_marker(value)).count();
|
||||
}
|
||||
|
||||
fn raw_log_message_is_exact_truncation_marker(value: &serde_json::Value) -> bool {
|
||||
return matches!(value, serde_json::Value::String(line) if line == "Log truncated");
|
||||
}
|
||||
|
||||
fn raw_transaction_content_conflict_diagnostic(
|
||||
stored: &ksp_store_api::RawTransaction,
|
||||
incoming: &ksp_store_api::RawTransaction,
|
||||
@@ -1374,6 +1451,25 @@ fn json_object_other_fields_mismatch(
|
||||
return stored_other != incoming_other;
|
||||
}
|
||||
|
||||
fn log_raw_transaction_compatible_truncated_log_messages(
|
||||
network: &ksp_store_api::RawNetworkId,
|
||||
stored: &ksp_store_api::RawTransaction,
|
||||
incoming: &ksp_store_api::RawTransaction,
|
||||
) {
|
||||
let diagnostic = raw_transaction_content_conflict_diagnostic(stored, incoming);
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = "store.raw_transaction.compatible_partial",
|
||||
network = network.as_str(),
|
||||
slot = stored.slot(),
|
||||
stored_log_messages_count = diagnostic.log_messages.stored_count,
|
||||
incoming_log_messages_count = diagnostic.log_messages.incoming_count,
|
||||
incoming_log_messages_has_truncation_marker = diagnostic.log_messages.incoming_has_truncation_marker,
|
||||
"PostgreSQL Store accepted compatible truncated RAW transaction logs without replacing canonical content"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
fn log_raw_transaction_content_conflict(
|
||||
network: &ksp_store_api::RawNetworkId,
|
||||
stored: &ksp_store_api::RawTransaction,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-store-postgres-lib/tests/hardening_completeness.rs
|
||||
// version: 25
|
||||
// version: 26
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -643,3 +643,23 @@ fn v0_3_15_pre_014_content_conflict_diagnostic_exposes_only_block_slot_and_bound
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v0_3_15_pre_014_fix_001_truncated_log_compatibility_is_narrow_and_keeps_canonical_content() {
|
||||
let source = include_str!("../src/raw_transaction.rs");
|
||||
for required in [
|
||||
"ActiveIncomingTruncatedLogs",
|
||||
"raw_transaction_incoming_truncated_log_messages_compatible",
|
||||
"raw_meta_other_than_log_messages_mismatch",
|
||||
"raw_log_messages_incoming_truncated_compatible",
|
||||
"raw_log_message_is_exact_truncation_marker",
|
||||
"marker_index >= stored.len()",
|
||||
"incoming[..marker_index] != stored[..marker_index]",
|
||||
"PostgreSQL Store accepted compatible truncated RAW transaction logs without replacing canonical content",
|
||||
] {
|
||||
assert!(source.contains(required), "missing narrow truncated-log compatibility guard: {required}");
|
||||
}
|
||||
assert!(!source.contains("UPDATE ksp_raw_transactions SET payload"));
|
||||
assert!(!source.contains("DELETE FROM ksp_raw_transactions"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-store-postgres-lib/unit_tests/raw_transaction.rs
|
||||
// version: 8
|
||||
// version: 9
|
||||
|
||||
fn network() -> ksp_store_api::RawNetworkId {
|
||||
return match ksp_store_api::RawNetworkId::new("devnet") {
|
||||
@@ -457,6 +457,72 @@ fn v0_3_15_pre_014_log_messages_conflict_diagnostic_distinguishes_prefix_truncat
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v0_3_15_pre_014_fix_001_incoming_truncated_logs_are_compatible_only_when_missing_middle_is_proven() {
|
||||
let stored = raw_transaction(
|
||||
9,
|
||||
br#"{"transaction":["AQID","base64"],"meta":{"err":null,"fee":5000,"logMessages":["Program Vote111 invoke [1]","Program log: stable","Program log: detail-a","Program log: detail-b","Program Vote111 success"],"rewards":[]},"version":0,"transactionIndex":7}"#,
|
||||
7,
|
||||
);
|
||||
let incoming = raw_transaction(
|
||||
9,
|
||||
br#"{"transaction":["AQID","base64"],"meta":{"err":null,"fee":5000,"logMessages":["Program Vote111 invoke [1]","Program log: stable","Log truncated","Program Vote111 success"],"rewards":[]},"version":0,"transactionIndex":7}"#,
|
||||
8,
|
||||
);
|
||||
assert!(super::raw_transaction_incoming_truncated_log_messages_compatible(&stored, &incoming));
|
||||
let mut stored_row = transaction_row("full");
|
||||
stored_row.signature = stored.reference().signature().as_bytes().to_vec();
|
||||
stored_row.slot_text = stored.slot().to_string();
|
||||
stored_row.block_time_unix_millis = stored.block_time().map(|value| return value.unix_millis());
|
||||
stored_row.format_id = stored.payload().format_id().as_str().to_owned();
|
||||
stored_row.format_version = i64::from(stored.payload().format_version());
|
||||
stored_row.content_hash = stored.payload().content_hash().as_bytes().to_vec();
|
||||
stored_row.payload = std::option::Option::Some(stored.payload().bytes().to_vec());
|
||||
let comparison = super::compare_existing_transaction(&network(), stored_row, &incoming);
|
||||
assert!(matches!(comparison, std::result::Result::Ok(super::ExistingTransactionMatch::ActiveIncomingTruncatedLogs)));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v0_3_15_pre_014_fix_001_truncated_log_compatibility_rejects_unproven_or_reverse_changes() {
|
||||
let stored = raw_transaction(
|
||||
9,
|
||||
br#"{"transaction":["AQID","base64"],"meta":{"err":null,"fee":5000,"logMessages":["Program Vote111 invoke [1]","Program log: stable","Program log: detail-a","Program log: detail-b","Program Vote111 success"],"rewards":[]},"version":0,"transactionIndex":7}"#,
|
||||
7,
|
||||
);
|
||||
let bad_prefix = raw_transaction(
|
||||
9,
|
||||
br#"{"transaction":["AQID","base64"],"meta":{"err":null,"fee":5000,"logMessages":["Program Vote111 invoke [1]","Program log: changed","Log truncated","Program Vote111 success"],"rewards":[]},"version":0,"transactionIndex":7}"#,
|
||||
8,
|
||||
);
|
||||
assert!(!super::raw_transaction_incoming_truncated_log_messages_compatible(&stored, &bad_prefix));
|
||||
let post_marker_material = raw_transaction(
|
||||
9,
|
||||
br#"{"transaction":["AQID","base64"],"meta":{"err":null,"fee":5000,"logMessages":["Program Vote111 invoke [1]","Program log: stable","Log truncated","Program Other111 success"],"rewards":[]},"version":0,"transactionIndex":7}"#,
|
||||
8,
|
||||
);
|
||||
assert!(super::raw_transaction_incoming_truncated_log_messages_compatible(&stored, &post_marker_material));
|
||||
let other_meta = raw_transaction(
|
||||
9,
|
||||
br#"{"transaction":["AQID","base64"],"meta":{"err":null,"fee":5001,"logMessages":["Program Vote111 invoke [1]","Program log: stable","Log truncated","Program Vote111 success"],"rewards":[]},"version":0,"transactionIndex":7}"#,
|
||||
8,
|
||||
);
|
||||
assert!(!super::raw_transaction_incoming_truncated_log_messages_compatible(&stored, &other_meta));
|
||||
let non_exact_marker = raw_transaction(
|
||||
9,
|
||||
br#"{"transaction":["AQID","base64"],"meta":{"err":null,"fee":5000,"logMessages":["Program Vote111 invoke [1]","Program log: stable","Log truncated by provider"],"rewards":[]},"version":0,"transactionIndex":7}"#,
|
||||
8,
|
||||
);
|
||||
assert!(!super::raw_transaction_incoming_truncated_log_messages_compatible(&stored, &non_exact_marker));
|
||||
let stored_truncated = raw_transaction(
|
||||
9,
|
||||
br#"{"transaction":["AQID","base64"],"meta":{"err":null,"fee":5000,"logMessages":["Program Vote111 invoke [1]","Program log: stable","Log truncated","Program Vote111 success"],"rewards":[]},"version":0,"transactionIndex":7}"#,
|
||||
8,
|
||||
);
|
||||
assert!(!super::raw_transaction_incoming_truncated_log_messages_compatible(&stored_truncated, &stored));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v0_3_15_pre_014_log_messages_conflict_diagnostic_detects_strict_prefix_without_exposing_line_material() {
|
||||
let stored = raw_transaction(
|
||||
|
||||
Reference in New Issue
Block a user