0.3.15-pre.014

This commit is contained in:
2026-09-17 13:02:10 +02:00
parent 6ee4e3bdf4
commit ad24cb36fa
10 changed files with 561 additions and 13 deletions

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/src/raw_transaction.rs
// version: 8
// version: 9
pub(crate) mod cursor;
@@ -48,8 +48,26 @@ const UPDATE_ARCHIVED_TRANSACTION_SQL: &str =
"UPDATE ksp_raw_transactions SET payload = NULL, retention_state = 'archived' WHERE signature = $1 AND retention_state = 'full'";
const UPDATE_PURGED_TRANSACTION_SQL: &str = "UPDATE ksp_raw_transactions SET block_time_unix_millis = NULL, payload = NULL, retention_state = 'purged' WHERE signature = $1 AND retention_state = 'archived'";
struct RawLogMessagesContentConflictDiagnostic {
available: bool,
first_mismatch_index: std::string::String,
incoming_count: usize,
incoming_first_kind: &'static str,
incoming_first_length: usize,
incoming_has_truncation_marker: bool,
incoming_prefix_of_stored: bool,
incoming_state: &'static str,
stored_count: usize,
stored_first_kind: &'static str,
stored_first_length: usize,
stored_has_truncation_marker: bool,
stored_prefix_of_incoming: bool,
stored_state: &'static str,
}
struct RawPayloadContentConflictDiagnostic {
available: bool,
log_messages: RawLogMessagesContentConflictDiagnostic,
meta_mismatch: bool,
meta_mismatch_fields: std::string::String,
other_mismatch: bool,
@@ -63,6 +81,7 @@ struct RawTransactionContentConflictDiagnostic {
content_hash_mismatch: bool,
format_id_mismatch: bool,
format_version_mismatch: bool,
log_messages: RawLogMessagesContentConflictDiagnostic,
meta_mismatch_fields: std::string::String,
payload_bytes_mismatch: bool,
payload_diagnostic_available: bool,
@@ -1138,6 +1157,7 @@ fn raw_transaction_content_conflict_diagnostic(
content_hash_mismatch: stored.payload().content_hash() != incoming.payload().content_hash(),
format_id_mismatch: stored.payload().format_id() != incoming.payload().format_id(),
format_version_mismatch: stored.payload().format_version() != incoming.payload().format_version(),
log_messages: payload.log_messages,
meta_mismatch_fields: payload.meta_mismatch_fields,
payload_bytes_mismatch: stored.payload().bytes() != incoming.payload().bytes(),
payload_diagnostic_available: payload.available,
@@ -1158,6 +1178,7 @@ fn raw_payload_content_conflict_diagnostic(stored: &[u8], incoming: &[u8]) -> Ra
_ => {
return RawPayloadContentConflictDiagnostic {
available: false,
log_messages: raw_log_messages_content_conflict_diagnostic(std::option::Option::None, std::option::Option::None),
meta_mismatch: false,
meta_mismatch_fields: "unavailable".to_owned(),
other_mismatch: false,
@@ -1173,8 +1194,10 @@ fn raw_payload_content_conflict_diagnostic(stored: &[u8], incoming: &[u8]) -> Ra
let transaction_index_mismatch = stored.get("transactionIndex") != incoming.get("transactionIndex");
let other_mismatch = json_object_other_fields_mismatch(&stored, &incoming, RAW_TRANSACTION_CONTENT_CONFLICT_PAYLOAD_FIELDS.as_slice());
let meta_mismatch_fields = raw_meta_content_conflict_fields(stored.get("meta"), incoming.get("meta"));
let log_messages = raw_log_messages_content_conflict_diagnostic(stored.get("meta"), incoming.get("meta"));
return RawPayloadContentConflictDiagnostic {
available: true,
log_messages,
meta_mismatch,
meta_mismatch_fields,
other_mismatch,
@@ -1184,6 +1207,140 @@ fn raw_payload_content_conflict_diagnostic(stored: &[u8], incoming: &[u8]) -> Ra
};
}
fn raw_log_messages_content_conflict_diagnostic(
stored_meta: std::option::Option<&serde_json::Value>,
incoming_meta: std::option::Option<&serde_json::Value>,
) -> RawLogMessagesContentConflictDiagnostic {
let stored = raw_log_messages_value(stored_meta);
let incoming = raw_log_messages_value(incoming_meta);
let stored_state = raw_log_messages_state(stored);
let incoming_state = raw_log_messages_state(incoming);
let (stored_values, incoming_values) = match (stored, incoming) {
(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 RawLogMessagesContentConflictDiagnostic {
available: false,
first_mismatch_index: "unavailable".to_owned(),
incoming_count: raw_log_messages_count(incoming),
incoming_first_kind: "unavailable",
incoming_first_length: 0,
incoming_has_truncation_marker: raw_log_messages_has_truncation_marker(incoming),
incoming_prefix_of_stored: false,
incoming_state,
stored_count: raw_log_messages_count(stored),
stored_first_kind: "unavailable",
stored_first_length: 0,
stored_has_truncation_marker: raw_log_messages_has_truncation_marker(stored),
stored_prefix_of_incoming: false,
stored_state,
};
},
};
let shared_len = stored_values.len().min(incoming_values.len());
let first_mismatch = stored_values.iter().zip(incoming_values.iter()).position(|(stored, incoming)| return stored != incoming).or_else(|| {
if stored_values.len() == incoming_values.len() {
return std::option::Option::None;
}
return std::option::Option::Some(shared_len);
});
let (stored_first_kind, stored_first_length) = raw_log_message_kind_and_length(first_mismatch.and_then(|index| return stored_values.get(index)));
let (incoming_first_kind, incoming_first_length) = raw_log_message_kind_and_length(first_mismatch.and_then(|index| return incoming_values.get(index)));
let first_mismatch_index = match first_mismatch {
std::option::Option::Some(value) => value.to_string(),
std::option::Option::None => "none".to_owned(),
};
return RawLogMessagesContentConflictDiagnostic {
available: true,
first_mismatch_index,
incoming_count: incoming_values.len(),
incoming_first_kind,
incoming_first_length,
incoming_has_truncation_marker: raw_log_message_array_has_truncation_marker(incoming_values),
incoming_prefix_of_stored: raw_json_array_is_prefix(incoming_values, stored_values),
incoming_state,
stored_count: stored_values.len(),
stored_first_kind,
stored_first_length,
stored_has_truncation_marker: raw_log_message_array_has_truncation_marker(stored_values),
stored_prefix_of_incoming: raw_json_array_is_prefix(stored_values, incoming_values),
stored_state,
};
}
fn raw_log_messages_value(meta: std::option::Option<&serde_json::Value>) -> std::option::Option<&serde_json::Value> {
return match meta {
std::option::Option::Some(serde_json::Value::Object(meta)) => meta.get("logMessages"),
_ => std::option::Option::None,
};
}
fn raw_log_messages_state(value: std::option::Option<&serde_json::Value>) -> &'static str {
return match value {
std::option::Option::None => "missing",
std::option::Option::Some(serde_json::Value::Null) => "null",
std::option::Option::Some(serde_json::Value::Array(_)) => "array",
std::option::Option::Some(_) => "other",
};
}
fn raw_log_messages_count(value: std::option::Option<&serde_json::Value>) -> usize {
return match value {
std::option::Option::Some(serde_json::Value::Array(values)) => values.len(),
_ => 0,
};
}
fn raw_json_array_is_prefix(prefix: &[serde_json::Value], values: &[serde_json::Value]) -> bool {
if prefix.len() > values.len() {
return false;
}
return prefix.iter().zip(values.iter()).all(|(left, right)| return left == right);
}
fn raw_log_message_kind_and_length(value: std::option::Option<&serde_json::Value>) -> (&'static str, usize) {
let line = match value {
std::option::Option::Some(serde_json::Value::String(value)) => value.as_str(),
std::option::Option::Some(_) => return ("non_string", 0),
std::option::Option::None => return ("missing", 0),
};
let kind = if line.starts_with("Log truncated") || line.starts_with("log truncated") {
"log_truncated"
} else if line.starts_with("Program log:") {
"program_log"
} else if line.starts_with("Program data:") {
"program_data"
} else if line.starts_with("Program ") && line.contains(" invoke [") {
"program_invoke"
} else if line.starts_with("Program ") && line.ends_with(" success") {
"program_success"
} else if line.starts_with("Program ") && line.contains(" failed:") {
"program_failed"
} else if line.starts_with("Program ") && line.contains(" consumed ") && line.contains(" compute units") {
"compute_units"
} else {
"other"
};
return (kind, line.len());
}
fn raw_log_messages_has_truncation_marker(value: std::option::Option<&serde_json::Value>) -> bool {
return match value {
std::option::Option::Some(serde_json::Value::Array(values)) => raw_log_message_array_has_truncation_marker(values.as_slice()),
_ => false,
};
}
fn raw_log_message_array_has_truncation_marker(values: &[serde_json::Value]) -> bool {
return values.iter().any(|value| {
return match value {
serde_json::Value::String(line) => line.starts_with("Log truncated") || line.starts_with("log truncated"),
_ => false,
};
});
}
fn raw_meta_content_conflict_fields(stored: std::option::Option<&serde_json::Value>, incoming: std::option::Option<&serde_json::Value>) -> std::string::String {
if stored == incoming {
return "none".to_owned();
@@ -1227,6 +1384,8 @@ fn log_raw_transaction_content_conflict(
target: crate::TRACING_TARGET,
domain = "store.raw_transaction.content_conflict",
network = network.as_str(),
stored_slot = stored.slot(),
incoming_slot = incoming.slot(),
slot_mismatch = diagnostic.slot_mismatch,
block_time_mismatch = diagnostic.block_time_mismatch,
format_id_mismatch = diagnostic.format_id_mismatch,
@@ -1240,6 +1399,20 @@ fn log_raw_transaction_content_conflict(
transaction_index_mismatch = diagnostic.payload_transaction_index_mismatch,
payload_other_mismatch = diagnostic.payload_other_mismatch,
meta_mismatch_fields = diagnostic.meta_mismatch_fields.as_str(),
log_messages_diagnostic_available = diagnostic.log_messages.available,
stored_log_messages_state = diagnostic.log_messages.stored_state,
incoming_log_messages_state = diagnostic.log_messages.incoming_state,
stored_log_messages_count = diagnostic.log_messages.stored_count,
incoming_log_messages_count = diagnostic.log_messages.incoming_count,
log_messages_first_mismatch_index = diagnostic.log_messages.first_mismatch_index.as_str(),
stored_log_messages_first_kind = diagnostic.log_messages.stored_first_kind,
incoming_log_messages_first_kind = diagnostic.log_messages.incoming_first_kind,
stored_log_messages_first_length = diagnostic.log_messages.stored_first_length,
incoming_log_messages_first_length = diagnostic.log_messages.incoming_first_length,
stored_log_messages_prefix_of_incoming = diagnostic.log_messages.stored_prefix_of_incoming,
incoming_log_messages_prefix_of_stored = diagnostic.log_messages.incoming_prefix_of_stored,
stored_log_messages_has_truncation_marker = diagnostic.log_messages.stored_has_truncation_marker,
incoming_log_messages_has_truncation_marker = diagnostic.log_messages.incoming_has_truncation_marker,
"PostgreSQL Store rejected divergent canonical RAW transaction"
);
return;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/tests/hardening_completeness.rs
// version: 24
// version: 25
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -606,3 +606,40 @@ fn v0_3_15_pre_012_fix_004_content_conflict_diagnostic_is_field_only_bounded_and
assert!(source.contains("fields.push(\"other\")"));
return;
}
#[test]
fn v0_3_15_pre_014_content_conflict_diagnostic_exposes_only_block_slot_and_bounded_log_shape() {
let source = include_str!("../src/raw_transaction.rs");
for required in [
"stored_slot",
"incoming_slot",
"log_messages_first_mismatch_index",
"stored_log_messages_count",
"incoming_log_messages_count",
"stored_log_messages_first_kind",
"incoming_log_messages_first_kind",
"stored_log_messages_prefix_of_incoming",
"incoming_log_messages_prefix_of_stored",
"stored_log_messages_has_truncation_marker",
"incoming_log_messages_has_truncation_marker",
"raw_log_message_kind_and_length",
"raw_log_message_array_has_truncation_marker",
] {
assert!(source.contains(required), "missing pre.014 bounded logMessages diagnostic field: {required}");
}
let start = source.find("fn log_raw_transaction_content_conflict(");
assert!(start.is_some(), "missing pre.014 conflict logger");
let Some(start) = start else {
return;
};
let end = source[start..].find("async fn log_raw_transaction_content_conflict_provenance(");
assert!(end.is_some(), "missing pre.014 conflict logger end marker");
let Some(end) = end else {
return;
};
let logger = &source[start..start + end];
for forbidden in ["signature =", "blockhash =", "previous_blockhash =", "log_message =", "log_line =", "payload ="] {
assert!(!logger.contains(forbidden), "pre.014 conflict logger exposes forbidden material: {forbidden}");
}
return;
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/unit_tests/raw_transaction.rs
// version: 7
// version: 8
fn network() -> ksp_store_api::RawNetworkId {
return match ksp_store_api::RawNetworkId::new("devnet") {
@@ -429,6 +429,52 @@ fn v0_3_15_pre_012_fix_004_content_conflict_diagnostic_collapses_unknown_meta_ke
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);

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-worker-raw-transaction-ingest-lib/src/runtime_resources.rs
// version: 48
// version: 49
use sha2::Digest; // rust-rules: trait-import
@@ -19,6 +19,7 @@ pub const MIN_RAW_TRANSACTION_INGEST_HTTP_POLL_INTERVAL: std::time::Duration = s
pub const MIN_RAW_TRANSACTION_INGEST_HTTP_POLL_MAX_BLOCKS_PER_CYCLE: u16 = 1;
const MAX_RAW_TRANSACTION_INGEST_REPAIR_BURST: usize = 1;
const RAW_TRANSACTION_INGEST_BLOCK_IDENTITY_FINGERPRINT_DOMAIN: &[u8] = b"ksp.raw_transaction_ingest.block_identity.v1\0";
const RAW_TRANSACTION_INGEST_HELIUS_TRANSACTION_FILTER_FINGERPRINT_DOMAIN: &[u8] = b"ksp.raw_transaction_ingest.helius_transaction.filter.v1\0";
const RAW_TRANSACTION_INGEST_HELIUS_TRANSACTION_HTTP_PROTOCOL: &str = "helius_ws_http";
const RAW_TRANSACTION_INGEST_HELIUS_TRANSACTION_HTTP_SOURCE_KEY_DOMAIN: &[u8] = b"ksp.raw_transaction_ingest.helius_transaction_http.source_key.v1\0";
@@ -3595,6 +3596,7 @@ async fn fetch_yellowstone_block_ingresses(
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(crate::runtime_error("source.yellowstone_block_not_available")),
};
log_block_identity("yellowstone_block_hydration", &hydration.network, slot, hydration.commitment, observed.endpoint_name(), observed.provider(), block);
let received_at = match current_raw_timestamp() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
@@ -3665,6 +3667,7 @@ fn project_http_block_polling_ingresses(
observed: &ksp_onchain_transport_lib::HttpObservedValue<std::option::Option<ksp_onchain_transport_lib::SolanaConfirmedBlock>>,
received_at: ksp_store_lib::RawTimestamp,
) -> ksp_core_lib::Result<std::vec::Vec<crate::RawTransactionIngress>> {
log_block_identity("http_block_polling", &source.network, slot, source.commitment, observed.endpoint_name(), observed.provider(), block);
let transactions = match block.transactions() {
ksp_onchain_transport_lib::SolanaWireField::Value(value) => value,
ksp_onchain_transport_lib::SolanaWireField::Omitted | ksp_onchain_transport_lib::SolanaWireField::Null => {
@@ -5573,6 +5576,61 @@ fn hydration_method_code(family: RawTransactionIngestSourceFamily) -> &'static s
};
}
fn block_identity_fingerprint(block: &ksp_onchain_transport_lib::SolanaConfirmedBlock) -> std::string::String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut hasher = sha2::Sha256::new();
hasher.update(RAW_TRANSACTION_INGEST_BLOCK_IDENTITY_FINGERPRINT_DOMAIN);
hash_live_source_key_component(&mut hasher, block.blockhash().as_bytes());
hash_live_source_key_component(&mut hasher, block.previous_blockhash().as_bytes());
hasher.update(block.parent_slot().to_be_bytes());
match block.block_height() {
std::option::Option::Some(value) => {
hasher.update([1_u8]);
hasher.update(value.to_be_bytes());
},
std::option::Option::None => hasher.update([0_u8]),
}
let digest: [u8; 32] = hasher.finalize().into();
let mut value = std::string::String::with_capacity(71);
value.push_str("sha256.");
for byte in digest {
value.push(char::from(HEX[usize::from(byte >> 4)]));
value.push(char::from(HEX[usize::from(byte & 0x0f)]));
}
return value;
}
fn log_block_identity(
source_kind: &'static str,
network: &ksp_store_lib::RawNetworkId,
slot: u64,
commitment: ksp_onchain_transport_lib::SolanaCommitment,
endpoint_name: &str,
provider_name: &ksp_onchain_transport_lib::HttpProviderName,
block: &ksp_onchain_transport_lib::SolanaConfirmedBlock,
) {
let fingerprint = block_identity_fingerprint(block);
let block_height = match block.block_height() {
std::option::Option::Some(value) => value.to_string(),
std::option::Option::None => "none".to_owned(),
};
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = "raw_transaction_ingest.block_identity",
source_kind = source_kind,
network = network.as_str(),
slot = slot,
provider = provider_name.as_str(),
endpoint_id = endpoint_name,
commitment = commitment.as_str(),
parent_slot = block.parent_slot(),
block_height = block_height.as_str(),
block_identity_fingerprint = fingerprint.as_str(),
"RAW transaction ingest observed HTTP block identity"
);
return;
}
fn fingerprint_filter_code(fingerprint: &[u8; 32]) -> ksp_core_lib::Result<ksp_store_lib::RawProvenanceCode> {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut value = std::string::String::with_capacity(71);

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-worker-raw-transaction-ingest-lib/tests/hardening.rs
// version: 39
// version: 40
//! External public, security, redaction and release-boundary hardening canaries through `v0.3.15-pre.004`.
@@ -1449,3 +1449,35 @@ fn v0_3_15_pre_013_yellowstone_block_hydration_is_bounded_concurrent_and_stop_pr
}
return;
}
#[test]
fn v0_3_15_pre_014_cross_provider_block_identity_diagnostic_is_safe_and_covers_both_get_block_routes() {
let resources = include_str!("../src/runtime_resources.rs");
for required in [
"RAW_TRANSACTION_INGEST_BLOCK_IDENTITY_FINGERPRINT_DOMAIN",
"fn block_identity_fingerprint(",
"fn log_block_identity(",
"domain = \"raw_transaction_ingest.block_identity\"",
"\"yellowstone_block_hydration\"",
"\"http_block_polling\"",
"block_identity_fingerprint = fingerprint.as_str()",
"parent_slot = block.parent_slot()",
] {
assert!(resources.contains(required), "missing pre.014 block identity diagnostic guard: {required}");
}
let start = resources.find("fn log_block_identity(");
assert!(start.is_some(), "missing pre.014 block identity logger");
let Some(start) = start else {
return;
};
let end = resources[start..].find("fn fingerprint_filter_code(");
assert!(end.is_some(), "missing pre.014 block identity logger end marker");
let Some(end) = end else {
return;
};
let logger = &resources[start..start + end];
for forbidden in ["blockhash =", "previous_blockhash =", "signature =", "payload =", "transaction ="] {
assert!(!logger.contains(forbidden), "pre.014 block identity logger exposes forbidden material: {forbidden}");
}
return;
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-worker-raw-transaction-ingest-lib/tests/release_completeness.rs
// version: 35
// version: 36
//! Release-completeness canaries through the `v0.3.15-pre.004` WebSocket capability-enforcement tranche.
@@ -496,3 +496,13 @@ fn v0_3_15_pre_013_yellowstone_block_backpressure_fix_adds_no_public_surface() {
assert!(!root.contains("YellowstoneBlockHydrationContext"));
return;
}
#[test]
fn v0_3_15_pre_014_cross_provider_convergence_diagnostic_adds_no_public_worker_surface() {
let hardening = include_str!("hardening.rs");
let root = include_str!("../src/lib.rs");
assert!(hardening.contains("v0_3_15_pre_014_cross_provider_block_identity_diagnostic_is_safe_and_covers_both_get_block_routes"));
assert!(!root.contains("BlockIdentityFingerprint"));
assert!(!root.contains("LogMessagesConflictDiagnostic"));
return;
}