0.3.16-pre.005
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-store-api/src/lib.rs
|
||||
// version: 8
|
||||
// version: 9
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
@@ -170,6 +170,8 @@ pub use self::model::raw_transaction::RawTransactionVariantReference;
|
||||
pub use self::model::raw_transaction::RawTransactionVariantRelation;
|
||||
/// Stable backend-neutral reason explaining one RAW transaction variant relation.
|
||||
pub use self::model::raw_transaction::RawTransactionVariantRelationReason;
|
||||
/// Compares one incoming RAW transaction variant against the currently canonical variant.
|
||||
pub use self::model::raw_transaction::compare_raw_transaction_variants;
|
||||
/// Common KSP error type used by Store-facing contracts.
|
||||
pub use ksp_core_lib::Error;
|
||||
/// Stable structured code identifying a KSP error category and condition.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-store-api/src/model/raw_transaction.rs
|
||||
// version: 3
|
||||
// version: 5
|
||||
|
||||
/// Durable backend-independent identity of one canonical RAW transaction.
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
@@ -203,6 +203,51 @@ impl RawTransactionVariantComparison {
|
||||
}
|
||||
}
|
||||
|
||||
/// Compares one incoming RAW transaction variant against the currently canonical variant.
|
||||
///
|
||||
/// The comparator is pure and backend-neutral. Automatic dominance is intentionally limited to
|
||||
/// a strictly proved `logMessages` truncation relation. Every other payload divergence fails closed
|
||||
/// as either [`RawTransactionVariantRelation::Conflict`] or [`RawTransactionVariantRelation::Incomparable`].
|
||||
pub fn compare_raw_transaction_variants(
|
||||
canonical: &crate::RawTransaction,
|
||||
incoming: &crate::RawTransaction,
|
||||
) -> crate::Result<crate::RawTransactionVariantComparison> {
|
||||
if canonical.reference() != incoming.reference() {
|
||||
return std::result::Result::Err(raw_variant_model_error("comparison.reference"));
|
||||
}
|
||||
if canonical.slot() != incoming.slot() {
|
||||
return crate::RawTransactionVariantComparison::try_new(
|
||||
crate::RawTransactionVariantRelation::Conflict,
|
||||
crate::RawTransactionVariantRelationReason::SlotMismatch,
|
||||
);
|
||||
}
|
||||
if canonical.block_time() != incoming.block_time() {
|
||||
return crate::RawTransactionVariantComparison::try_new(
|
||||
crate::RawTransactionVariantRelation::Conflict,
|
||||
crate::RawTransactionVariantRelationReason::BlockTimeMismatch,
|
||||
);
|
||||
}
|
||||
if canonical.payload().format_id() != incoming.payload().format_id() || canonical.payload().format_version() != incoming.payload().format_version() {
|
||||
return crate::RawTransactionVariantComparison::try_new(
|
||||
crate::RawTransactionVariantRelation::Incomparable,
|
||||
crate::RawTransactionVariantRelationReason::PayloadFormatMismatch,
|
||||
);
|
||||
}
|
||||
if canonical.payload().bytes() == incoming.payload().bytes() {
|
||||
return crate::RawTransactionVariantComparison::try_new(
|
||||
crate::RawTransactionVariantRelation::Exact,
|
||||
crate::RawTransactionVariantRelationReason::ExactCanonicalContent,
|
||||
);
|
||||
}
|
||||
if canonical.payload().content_hash() == incoming.payload().content_hash() {
|
||||
return crate::RawTransactionVariantComparison::try_new(
|
||||
crate::RawTransactionVariantRelation::Conflict,
|
||||
crate::RawTransactionVariantRelationReason::ContentHashCollision,
|
||||
);
|
||||
}
|
||||
return compare_raw_variant_payloads(canonical.payload().bytes(), incoming.payload().bytes());
|
||||
}
|
||||
|
||||
/// Durable lifecycle status of one RAW transaction conflict case.
|
||||
#[non_exhaustive]
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
@@ -304,6 +349,384 @@ impl RawTransactionObservation {
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_RAW_VARIANT_JSON_DEPTH: usize = 128;
|
||||
const RAW_LOG_MESSAGES_KEY: &[u8] = b"\"logMessages\"";
|
||||
const RAW_META_KEY: &[u8] = b"\"meta\"";
|
||||
const RAW_TRUNCATION_MARKER_JSON: &[u8] = b"\"Log truncated\"";
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct RawVariantJsonMember<'a> {
|
||||
key: &'a [u8],
|
||||
value: &'a [u8],
|
||||
}
|
||||
|
||||
fn compare_raw_variant_payloads(canonical: &[u8], incoming: &[u8]) -> crate::Result<crate::RawTransactionVariantComparison> {
|
||||
let canonical_members = raw_variant_json_object_members(canonical);
|
||||
let incoming_members = raw_variant_json_object_members(incoming);
|
||||
let (canonical_members, incoming_members) = match (canonical_members, incoming_members) {
|
||||
(std::option::Option::Some(canonical_members), std::option::Option::Some(incoming_members)) => (canonical_members, incoming_members),
|
||||
_ => return raw_variant_incomparable(),
|
||||
};
|
||||
if canonical_members.len() != incoming_members.len() {
|
||||
return raw_variant_incomparable();
|
||||
}
|
||||
let mut canonical_meta = std::option::Option::None;
|
||||
let mut incoming_meta = std::option::Option::None;
|
||||
for (canonical_member, incoming_member) in canonical_members.iter().zip(incoming_members.iter()) {
|
||||
if canonical_member.key != incoming_member.key {
|
||||
return raw_variant_incomparable();
|
||||
}
|
||||
if canonical_member.key == RAW_META_KEY {
|
||||
canonical_meta = std::option::Option::Some(canonical_member.value);
|
||||
incoming_meta = std::option::Option::Some(incoming_member.value);
|
||||
continue;
|
||||
}
|
||||
if canonical_member.value != incoming_member.value {
|
||||
return raw_variant_concrete_difference(canonical_member.value, incoming_member.value);
|
||||
}
|
||||
}
|
||||
let (canonical_meta, incoming_meta) = match (canonical_meta, incoming_meta) {
|
||||
(std::option::Option::Some(canonical_meta), std::option::Option::Some(incoming_meta)) => (canonical_meta, incoming_meta),
|
||||
_ => return raw_variant_incomparable(),
|
||||
};
|
||||
return compare_raw_variant_meta(canonical_meta, incoming_meta);
|
||||
}
|
||||
|
||||
fn compare_raw_variant_meta(canonical: &[u8], incoming: &[u8]) -> crate::Result<crate::RawTransactionVariantComparison> {
|
||||
if canonical == incoming {
|
||||
return raw_variant_incomparable();
|
||||
}
|
||||
if canonical == b"null" || incoming == b"null" {
|
||||
return raw_variant_incomparable();
|
||||
}
|
||||
let canonical_members = raw_variant_json_object_members(canonical);
|
||||
let incoming_members = raw_variant_json_object_members(incoming);
|
||||
let (canonical_members, incoming_members) = match (canonical_members, incoming_members) {
|
||||
(std::option::Option::Some(canonical_members), std::option::Option::Some(incoming_members)) => (canonical_members, incoming_members),
|
||||
_ => return raw_variant_concrete_difference(canonical, incoming),
|
||||
};
|
||||
if canonical_members.len() != incoming_members.len() {
|
||||
return raw_variant_incomparable();
|
||||
}
|
||||
let mut canonical_logs = std::option::Option::None;
|
||||
let mut incoming_logs = std::option::Option::None;
|
||||
for (canonical_member, incoming_member) in canonical_members.iter().zip(incoming_members.iter()) {
|
||||
if canonical_member.key != incoming_member.key {
|
||||
return raw_variant_incomparable();
|
||||
}
|
||||
if canonical_member.key == RAW_LOG_MESSAGES_KEY {
|
||||
canonical_logs = std::option::Option::Some(canonical_member.value);
|
||||
incoming_logs = std::option::Option::Some(incoming_member.value);
|
||||
continue;
|
||||
}
|
||||
if canonical_member.value != incoming_member.value {
|
||||
return raw_variant_concrete_difference(canonical_member.value, incoming_member.value);
|
||||
}
|
||||
}
|
||||
let (canonical_logs, incoming_logs) = match (canonical_logs, incoming_logs) {
|
||||
(std::option::Option::Some(canonical_logs), std::option::Option::Some(incoming_logs)) => (canonical_logs, incoming_logs),
|
||||
_ => return raw_variant_incomparable(),
|
||||
};
|
||||
if canonical_logs == incoming_logs {
|
||||
return raw_variant_incomparable();
|
||||
}
|
||||
let canonical_items = raw_variant_json_array_items(canonical_logs);
|
||||
let incoming_items = raw_variant_json_array_items(incoming_logs);
|
||||
let (canonical_items, incoming_items) = match (canonical_items, incoming_items) {
|
||||
(std::option::Option::Some(canonical_items), std::option::Option::Some(incoming_items)) => (canonical_items, incoming_items),
|
||||
_ => return raw_variant_concrete_difference(canonical_logs, incoming_logs),
|
||||
};
|
||||
if raw_variant_log_messages_truncated(incoming_items.as_slice(), canonical_items.as_slice()) {
|
||||
return crate::RawTransactionVariantComparison::try_new(
|
||||
crate::RawTransactionVariantRelation::CompatibleLessComplete,
|
||||
crate::RawTransactionVariantRelationReason::IncomingLogMessagesTruncated,
|
||||
);
|
||||
}
|
||||
if raw_variant_log_messages_truncated(canonical_items.as_slice(), incoming_items.as_slice()) {
|
||||
return crate::RawTransactionVariantComparison::try_new(
|
||||
crate::RawTransactionVariantRelation::CompatibleMoreComplete,
|
||||
crate::RawTransactionVariantRelationReason::CanonicalLogMessagesTruncated,
|
||||
);
|
||||
}
|
||||
return raw_variant_concrete_difference(canonical_logs, incoming_logs);
|
||||
}
|
||||
|
||||
fn raw_variant_concrete_difference(canonical: &[u8], incoming: &[u8]) -> crate::Result<crate::RawTransactionVariantComparison> {
|
||||
if canonical == b"null" || incoming == b"null" {
|
||||
return raw_variant_incomparable();
|
||||
}
|
||||
return crate::RawTransactionVariantComparison::try_new(
|
||||
crate::RawTransactionVariantRelation::Conflict,
|
||||
crate::RawTransactionVariantRelationReason::CanonicalPayloadConflict,
|
||||
);
|
||||
}
|
||||
|
||||
fn raw_variant_incomparable() -> crate::Result<crate::RawTransactionVariantComparison> {
|
||||
return crate::RawTransactionVariantComparison::try_new(
|
||||
crate::RawTransactionVariantRelation::Incomparable,
|
||||
crate::RawTransactionVariantRelationReason::UnsupportedCanonicalDifference,
|
||||
);
|
||||
}
|
||||
|
||||
fn raw_variant_log_messages_truncated(truncated: &[&[u8]], full: &[&[u8]]) -> bool {
|
||||
if truncated.is_empty() || full.is_empty() {
|
||||
return false;
|
||||
}
|
||||
if !truncated.iter().all(|value| return raw_variant_json_is_string(value)) || !full.iter().all(|value| return raw_variant_json_is_string(value)) {
|
||||
return false;
|
||||
}
|
||||
let marker_count = truncated.iter().filter(|value| return **value == RAW_TRUNCATION_MARKER_JSON).count();
|
||||
if marker_count != 1 || truncated.last().copied() != std::option::Option::Some(RAW_TRUNCATION_MARKER_JSON) {
|
||||
return false;
|
||||
}
|
||||
if full.iter().any(|value| return *value == RAW_TRUNCATION_MARKER_JSON) {
|
||||
return false;
|
||||
}
|
||||
let prefix_len = truncated.len() - 1;
|
||||
if prefix_len >= full.len() {
|
||||
return false;
|
||||
}
|
||||
return truncated[..prefix_len] == full[..prefix_len];
|
||||
}
|
||||
|
||||
fn raw_variant_json_object_members(value: &[u8]) -> std::option::Option<std::vec::Vec<RawVariantJsonMember<'_>>> {
|
||||
if std::str::from_utf8(value).is_err() {
|
||||
return std::option::Option::None;
|
||||
}
|
||||
let start = raw_variant_json_skip_whitespace(value, 0);
|
||||
if value.get(start) != std::option::Option::Some(&b'{') {
|
||||
return std::option::Option::None;
|
||||
}
|
||||
let end = match raw_variant_json_value_end(value, start, 0) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::option::Option::None,
|
||||
};
|
||||
if raw_variant_json_skip_whitespace(value, end) != value.len() {
|
||||
return std::option::Option::None;
|
||||
}
|
||||
let mut index = raw_variant_json_skip_whitespace(value, start + 1);
|
||||
let mut members = std::vec::Vec::new();
|
||||
if value.get(index) == std::option::Option::Some(&b'}') {
|
||||
return std::option::Option::Some(members);
|
||||
}
|
||||
loop {
|
||||
let key_start = index;
|
||||
let key_end = match raw_variant_json_string_end(value, key_start) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::option::Option::None,
|
||||
};
|
||||
index = raw_variant_json_skip_whitespace(value, key_end);
|
||||
if value.get(index) != std::option::Option::Some(&b':') {
|
||||
return std::option::Option::None;
|
||||
}
|
||||
index = raw_variant_json_skip_whitespace(value, index + 1);
|
||||
let value_start = index;
|
||||
let value_end = match raw_variant_json_value_end(value, value_start, 1) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::option::Option::None,
|
||||
};
|
||||
members.push(RawVariantJsonMember { key: &value[key_start..key_end], value: &value[value_start..value_end] });
|
||||
index = raw_variant_json_skip_whitespace(value, value_end);
|
||||
match value.get(index).copied() {
|
||||
std::option::Option::Some(b',') => index = raw_variant_json_skip_whitespace(value, index + 1),
|
||||
std::option::Option::Some(b'}') => return std::option::Option::Some(members),
|
||||
_ => return std::option::Option::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn raw_variant_json_array_items(value: &[u8]) -> std::option::Option<std::vec::Vec<&[u8]>> {
|
||||
let start = raw_variant_json_skip_whitespace(value, 0);
|
||||
if value.get(start) != std::option::Option::Some(&b'[') {
|
||||
return std::option::Option::None;
|
||||
}
|
||||
let end = match raw_variant_json_value_end(value, start, 0) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::option::Option::None,
|
||||
};
|
||||
if raw_variant_json_skip_whitespace(value, end) != value.len() {
|
||||
return std::option::Option::None;
|
||||
}
|
||||
let mut index = raw_variant_json_skip_whitespace(value, start + 1);
|
||||
let mut items = std::vec::Vec::new();
|
||||
if value.get(index) == std::option::Option::Some(&b']') {
|
||||
return std::option::Option::Some(items);
|
||||
}
|
||||
loop {
|
||||
let item_start = index;
|
||||
let item_end = match raw_variant_json_value_end(value, item_start, 1) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::option::Option::None,
|
||||
};
|
||||
items.push(&value[item_start..item_end]);
|
||||
index = raw_variant_json_skip_whitespace(value, item_end);
|
||||
match value.get(index).copied() {
|
||||
std::option::Option::Some(b',') => index = raw_variant_json_skip_whitespace(value, index + 1),
|
||||
std::option::Option::Some(b']') => return std::option::Option::Some(items),
|
||||
_ => return std::option::Option::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn raw_variant_json_value_end(value: &[u8], start: usize, depth: usize) -> std::option::Option<usize> {
|
||||
if depth > MAX_RAW_VARIANT_JSON_DEPTH {
|
||||
return std::option::Option::None;
|
||||
}
|
||||
let start = raw_variant_json_skip_whitespace(value, start);
|
||||
return match value.get(start).copied() {
|
||||
std::option::Option::Some(b'"') => raw_variant_json_string_end(value, start),
|
||||
std::option::Option::Some(b'{') => raw_variant_json_object_end(value, start, depth + 1),
|
||||
std::option::Option::Some(b'[') => raw_variant_json_array_end(value, start, depth + 1),
|
||||
std::option::Option::Some(b't') => raw_variant_json_literal_end(value, start, b"true"),
|
||||
std::option::Option::Some(b'f') => raw_variant_json_literal_end(value, start, b"false"),
|
||||
std::option::Option::Some(b'n') => raw_variant_json_literal_end(value, start, b"null"),
|
||||
std::option::Option::Some(b'-' | b'0'..=b'9') => raw_variant_json_number_end(value, start),
|
||||
_ => std::option::Option::None,
|
||||
};
|
||||
}
|
||||
|
||||
fn raw_variant_json_object_end(value: &[u8], start: usize, depth: usize) -> std::option::Option<usize> {
|
||||
let mut index = raw_variant_json_skip_whitespace(value, start + 1);
|
||||
if value.get(index) == std::option::Option::Some(&b'}') {
|
||||
return std::option::Option::Some(index + 1);
|
||||
}
|
||||
loop {
|
||||
index = match raw_variant_json_string_end(value, index) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::option::Option::None,
|
||||
};
|
||||
index = raw_variant_json_skip_whitespace(value, index);
|
||||
if value.get(index) != std::option::Option::Some(&b':') {
|
||||
return std::option::Option::None;
|
||||
}
|
||||
index = match raw_variant_json_value_end(value, index + 1, depth) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::option::Option::None,
|
||||
};
|
||||
index = raw_variant_json_skip_whitespace(value, index);
|
||||
match value.get(index).copied() {
|
||||
std::option::Option::Some(b',') => index = raw_variant_json_skip_whitespace(value, index + 1),
|
||||
std::option::Option::Some(b'}') => return std::option::Option::Some(index + 1),
|
||||
_ => return std::option::Option::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn raw_variant_json_array_end(value: &[u8], start: usize, depth: usize) -> std::option::Option<usize> {
|
||||
let mut index = raw_variant_json_skip_whitespace(value, start + 1);
|
||||
if value.get(index) == std::option::Option::Some(&b']') {
|
||||
return std::option::Option::Some(index + 1);
|
||||
}
|
||||
loop {
|
||||
index = match raw_variant_json_value_end(value, index, depth) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::option::Option::None,
|
||||
};
|
||||
index = raw_variant_json_skip_whitespace(value, index);
|
||||
match value.get(index).copied() {
|
||||
std::option::Option::Some(b',') => index = raw_variant_json_skip_whitespace(value, index + 1),
|
||||
std::option::Option::Some(b']') => return std::option::Option::Some(index + 1),
|
||||
_ => return std::option::Option::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn raw_variant_json_string_end(value: &[u8], start: usize) -> std::option::Option<usize> {
|
||||
if value.get(start) != std::option::Option::Some(&b'"') {
|
||||
return std::option::Option::None;
|
||||
}
|
||||
let mut index = start + 1;
|
||||
while index < value.len() {
|
||||
match value[index] {
|
||||
b'"' => return std::option::Option::Some(index + 1),
|
||||
b'\\' => {
|
||||
index += 1;
|
||||
let escape = match value.get(index).copied() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::option::Option::None,
|
||||
};
|
||||
if escape == b'u' {
|
||||
if index + 4 >= value.len() || !value[index + 1..=index + 4].iter().all(|byte| return byte.is_ascii_hexdigit()) {
|
||||
return std::option::Option::None;
|
||||
}
|
||||
index += 5;
|
||||
continue;
|
||||
}
|
||||
if !matches!(escape, b'"' | b'\\' | b'/' | b'b' | b'f' | b'n' | b'r' | b't') {
|
||||
return std::option::Option::None;
|
||||
}
|
||||
index += 1;
|
||||
},
|
||||
0x00..=0x1f => return std::option::Option::None,
|
||||
_ => index += 1,
|
||||
}
|
||||
}
|
||||
return std::option::Option::None;
|
||||
}
|
||||
|
||||
fn raw_variant_json_literal_end(value: &[u8], start: usize, literal: &[u8]) -> std::option::Option<usize> {
|
||||
let end = match start.checked_add(literal.len()) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::option::Option::None,
|
||||
};
|
||||
if value.get(start..end) == std::option::Option::Some(literal) {
|
||||
return std::option::Option::Some(end);
|
||||
}
|
||||
return std::option::Option::None;
|
||||
}
|
||||
|
||||
fn raw_variant_json_number_end(value: &[u8], start: usize) -> std::option::Option<usize> {
|
||||
let mut index = start;
|
||||
if value.get(index) == std::option::Option::Some(&b'-') {
|
||||
index += 1;
|
||||
}
|
||||
match value.get(index).copied() {
|
||||
std::option::Option::Some(b'0') => index += 1,
|
||||
std::option::Option::Some(b'1'..=b'9') => {
|
||||
index += 1;
|
||||
while matches!(value.get(index).copied(), std::option::Option::Some(b'0'..=b'9')) {
|
||||
index += 1;
|
||||
}
|
||||
},
|
||||
_ => return std::option::Option::None,
|
||||
}
|
||||
if value.get(index) == std::option::Option::Some(&b'.') {
|
||||
index += 1;
|
||||
let fraction_start = index;
|
||||
while matches!(value.get(index).copied(), std::option::Option::Some(b'0'..=b'9')) {
|
||||
index += 1;
|
||||
}
|
||||
if index == fraction_start {
|
||||
return std::option::Option::None;
|
||||
}
|
||||
}
|
||||
if matches!(value.get(index).copied(), std::option::Option::Some(b'e' | b'E')) {
|
||||
index += 1;
|
||||
if matches!(value.get(index).copied(), std::option::Option::Some(b'+' | b'-')) {
|
||||
index += 1;
|
||||
}
|
||||
let exponent_start = index;
|
||||
while matches!(value.get(index).copied(), std::option::Option::Some(b'0'..=b'9')) {
|
||||
index += 1;
|
||||
}
|
||||
if index == exponent_start {
|
||||
return std::option::Option::None;
|
||||
}
|
||||
}
|
||||
return std::option::Option::Some(index);
|
||||
}
|
||||
|
||||
fn raw_variant_json_skip_whitespace(value: &[u8], mut index: usize) -> usize {
|
||||
while matches!(value.get(index).copied(), std::option::Option::Some(b' ' | b'\n' | b'\r' | b'\t')) {
|
||||
index += 1;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
fn raw_variant_json_is_string(value: &[u8]) -> bool {
|
||||
return raw_variant_json_string_end(value, 0) == std::option::Option::Some(value.len());
|
||||
}
|
||||
|
||||
fn raw_variant_model_error(field: &'static str) -> crate::Error {
|
||||
return crate::Error::new(crate::ERROR_CODE_RAW_MODEL_INVALID, "invalid RAW transaction variant model").with_context("field", field);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user