v0.4.8-pre.007

This commit is contained in:
2026-08-06 00:52:08 +02:00
parent 80c439c988
commit 44088c71c9
122 changed files with 8903 additions and 3914 deletions

View File

@@ -1,611 +1,25 @@
// file: kb-lib/src/materializer/transaction/annotations.rs
// version: 5
// version: 6
//! Exact committed SPL Memo transaction annotation projection.
//! Committed Memo transaction annotation materializer component.
const ACCEPTED_FAMILIES: &[crate::MdEventFamily] = &[crate::MdEventFamily::Audit];
const MEMO_ENTRY_NAMES: &[&str] = &["add_memo", "memo_intent", "invalid_memo_attempt"];
mod constants;
mod materializer;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum MemoGeneration {
V1,
V3,
V4,
}
/// Stable committed Memo transaction annotation materializer.
pub use self::materializer::MtTransactionAnnotationMaterializer;
impl MemoGeneration {
fn from_event(event: &crate::MdDecodedProtocolEvent) -> std::option::Option<Self> {
if event.program_id.0 == kb_program_ids::SPL_MEMO_V1_PROGRAM_ID
&& event.surface_code.0 == "spl.memo.v1"
{
return std::option::Option::Some(Self::V1);
}
if event.program_id.0 == kb_program_ids::SPL_MEMO_V3_PROGRAM_ID
&& event.surface_code.0 == "spl.memo.v3"
{
return std::option::Option::Some(Self::V3);
}
if event.program_id.0 == kb_program_ids::SPL_MEMO_V4_PROGRAM_ID
&& event.surface_code.0 == "spl.memo.v4"
{
return std::option::Option::Some(Self::V4);
}
return std::option::Option::None;
}
fn code(self) -> &'static str {
return match self {
Self::V1 => "v1",
Self::V3 => "v3",
Self::V4 => "v4",
};
}
fn signer_status(self) -> &'static str {
return match self {
Self::V1 => "not_applicable",
Self::V3 | Self::V4 => "valid",
};
}
}
struct MemoProjection {
generation: MemoGeneration,
memo_text: std::string::String,
payload_length_bytes: u64,
payload_sha256: std::string::String,
required_signers: std::vec::Vec<std::string::String>,
observed_signers: std::vec::Vec<std::string::String>,
verified_signers: std::vec::Vec<std::string::String>,
signer_validation_status: std::string::String,
}
/// Stable transaction annotation materializer.
#[derive(Clone, Debug, Default)]
pub struct MtTransactionAnnotationMaterializer;
impl crate::MtMaterializer for crate::MtTransactionAnnotationMaterializer {
fn materializer_name(&self) -> &'static str {
return "kb-lib.materializer.transaction.annotations";
}
fn materializer_version(&self) -> &'static str {
return env!("CARGO_PKG_VERSION");
}
fn accepts_event(&self, event: &crate::MdDecodedProtocolEvent) -> bool {
return accepts_event(event);
}
fn materialize_event(
&self,
_event: &crate::MdDecodedProtocolEvent,
) -> kb_core::Result<std::vec::Vec<crate::MdMaterializedEvent>> {
return std::result::Result::Ok(std::vec::Vec::new());
}
}
impl crate::MtApiEventMaterializer for crate::MtTransactionAnnotationMaterializer {
fn identity(&self) -> crate::MtApiMaterializerIdentity {
return crate::MtApiMaterializerIdentity {
name: crate::MT_TRANSACTION_ANNOTATIONS_PROCESSOR_NAME.to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
};
}
fn accepted_families(&self) -> &'static [crate::MdEventFamily] {
return ACCEPTED_FAMILIES;
}
fn accepts_observation(&self, observation: &crate::DcApiDecodedObservation) -> bool {
return accepts_event(&observation.event);
}
fn transaction_policy(
&self,
_family: crate::MdEventFamily,
) -> crate::MtApiMaterializationTransactionPolicy {
return crate::MtApiMaterializationTransactionPolicy::SuccessfulCommittedOnly;
}
fn materialize(
&self,
observation: &crate::DcApiDecodedObservation,
) -> crate::MtApiMaterializerExecutionResult {
if !accepts_event(&observation.event) {
return crate::MtApiMaterializerExecutionResult::ignored();
}
if observation.transaction_failed || !observation.observation_committed {
return crate::MtApiMaterializerExecutionResult::refused(
"uncommitted_transaction_annotation_refused",
"failed or uncommitted Memo observations cannot create transaction annotations",
);
}
if observation.event.event_name.0 != "add_memo" {
return crate::MtApiMaterializerExecutionResult::refused(
"invalid_transaction_annotation_source_refused",
"only a committed add_memo observation can create a transaction annotation",
);
}
let projection_result = parse_projection(observation);
let projection = match projection_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(message) => {
return failed("memo_annotation_payload_invalid", message);
},
};
let idempotence_key = format!(
"spl_memo:{}:{}:{}:{}",
observation.event.signature.0,
observation.event.instruction_path.0,
observation.event.program_id.0,
projection.payload_sha256
);
let output = crate::MtApiMaterializedOutput {
output_key: "transaction_annotation:spl_memo:0".to_string(),
family: crate::MdMaterializedEventFamily::TransactionAnnotation,
payload_json: serde_json::json!({
"projectionVersion": crate::MT_TRANSACTION_ANNOTATIONS_PROJECTION_VERSION,
"domain": "transaction_annotation",
"annotationKind": "spl.memo",
"idempotenceKey": idempotence_key,
"signature": observation.event.signature.0.clone(),
"slot": observation.event.slot.0,
"instructionPath": observation.event.instruction_path.0.clone(),
"sourceKind": match observation.event.source_kind {
crate::MdEventSourceKind::InnerInstruction => "inner_instruction",
_ => "instruction"
},
"generation": projection.generation.code(),
"programId": observation.event.program_id.0.clone(),
"text": projection.memo_text,
"payloadLengthBytes": projection.payload_length_bytes,
"payloadSha256": projection.payload_sha256,
"signers": {
"validationStatus": projection.signer_validation_status,
"required": projection.required_signers,
"observed": projection.observed_signers,
"verified": projection.verified_signers
},
"committed": true,
"provenance": {
"processorName": crate::MT_TRANSACTION_ANNOTATIONS_PROCESSOR_NAME,
"processorVersion": env!("CARGO_PKG_VERSION"),
"sourceProtocol": observation.event.protocol_code.0.clone(),
"sourceSurface": observation.event.surface_code.0.clone(),
"sourceEventCode": observation.event.event_code.0.clone(),
"sourceEventKey": observation.event_key.clone()
}
}),
};
tracing::debug!(
target: crate::TRACING_TARGET_MATERIALIZER_TRANSACTION_ANNOTATIONS,
action = "materialize_transaction_annotation",
signature = %observation.event.signature.0,
instruction_path = %observation.event.instruction_path.0,
program_id = %observation.event.program_id.0,
generation = projection.generation.code(),
payload_length_bytes = projection.payload_length_bytes,
output_count = 1_usize,
"materialize committed SPL Memo transaction annotation"
);
return crate::MtApiMaterializerExecutionResult {
status: crate::MtApiMaterializerOutcomeStatus::Inserted,
outputs: std::vec![output],
diagnostics: std::vec::Vec::new(),
};
}
}
fn accepts_event(event: &crate::MdDecodedProtocolEvent) -> bool {
return event.event_family == crate::MdEventFamily::Audit
&& event.protocol_code.0 == "spl.memo"
&& (event.source_kind == crate::MdEventSourceKind::Instruction
|| event.source_kind == crate::MdEventSourceKind::InnerInstruction)
&& MemoGeneration::from_event(event).is_some()
&& MEMO_ENTRY_NAMES.contains(&event.event_name.0.as_str());
}
fn parse_projection(
observation: &crate::DcApiDecodedObservation,
) -> std::result::Result<MemoProjection, std::string::String> {
let generation = match MemoGeneration::from_event(&observation.event) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(
"Memo generation and Program ID do not match".to_string(),
);
},
};
let payload = &observation.payload_json;
if payload.get("committed").and_then(serde_json::Value::as_bool)
!= std::option::Option::Some(true)
{
return std::result::Result::Err("Memo payload is not marked committed".to_string());
}
if payload.get("transactionSucceeded").and_then(serde_json::Value::as_bool)
!= std::option::Option::Some(true)
|| payload.get("payloadComplete").and_then(serde_json::Value::as_bool)
!= std::option::Option::Some(true)
|| string_field(payload, "runtimeValidation")
!= std::option::Option::Some("proven_by_successful_transaction")
|| payload
.get("utf8Validation")
.and_then(|value| return string_field(value, "status"))
!= std::option::Option::Some("valid")
{
return std::result::Result::Err(
"Memo successful runtime validation proof is incomplete".to_string(),
);
}
if string_field(payload, "generation") != std::option::Option::Some(generation.code()) {
return std::result::Result::Err(
"Memo payload generation does not match its surface".to_string(),
);
}
if string_field(payload, "programId")
!= std::option::Option::Some(observation.event.program_id.0.as_str())
{
return std::result::Result::Err(
"Memo payload Program ID does not match its event".to_string(),
);
}
if string_field(payload, "instructionPath")
!= std::option::Option::Some(observation.event.instruction_path.0.as_str())
{
return std::result::Result::Err(
"Memo payload instruction path does not match its event".to_string(),
);
}
let memo_text = match string_field(payload, "memoText") {
std::option::Option::Some(value) => value.to_string(),
std::option::Option::None => {
return std::result::Result::Err("committed Memo has no UTF-8 text".to_string());
},
};
let payload_length_bytes =
match payload.get("payloadLengthBytes").and_then(serde_json::Value::as_u64) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err("committed Memo has no byte length".to_string());
},
};
let measured_length = match u64::try_from(memo_text.len()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(format!(
"Memo text length conversion failed: {error}"
));
},
};
if measured_length != payload_length_bytes {
return std::result::Result::Err(
"Memo byte length does not match its UTF-8 text".to_string(),
);
}
if payload_length_bytes > crate::MT_TRANSACTION_ANNOTATIONS_MAX_MEMO_PAYLOAD_BYTES {
return std::result::Result::Err(
"Memo byte length exceeds the projection limit".to_string(),
);
}
let payload_sha256 = match string_field(payload, "payloadSha256") {
std::option::Option::Some(value) if is_sha256(value) => value.to_string(),
_ => return std::result::Result::Err("Memo payload SHA-256 is invalid".to_string()),
};
let signer_validation = match payload.get("signerValidation") {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err("Memo signer validation is absent".to_string());
},
};
let signer_validation_status = match string_field(signer_validation, "status") {
std::option::Option::Some(value) if value == generation.signer_status() => {
value.to_string()
},
_ => {
return std::result::Result::Err(
"Memo signer validation status is inconsistent".to_string(),
);
},
};
let missing_signers = match string_array(signer_validation, "missingRequiredSigners") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(message) => return std::result::Result::Err(message),
};
if !missing_signers.is_empty() {
return std::result::Result::Err("committed Memo reports missing signers".to_string());
}
let required_signers = match string_array(signer_validation, "orderedRequiredSigners") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(message) => return std::result::Result::Err(message),
};
let observed_signers = match string_array(signer_validation, "orderedObservedSigners") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(message) => return std::result::Result::Err(message),
};
let verified_signers = if generation == MemoGeneration::V1 {
if !required_signers.is_empty() {
return std::result::Result::Err("Memo v1 cannot require signers".to_string());
}
std::vec::Vec::new()
} else {
if required_signers != observed_signers {
return std::result::Result::Err(
"committed Memo required and observed signers differ".to_string(),
);
}
observed_signers.clone()
};
return std::result::Result::Ok(MemoProjection {
generation,
memo_text,
payload_length_bytes,
payload_sha256,
required_signers,
observed_signers,
verified_signers,
signer_validation_status,
});
}
fn string_field<'a>(value: &'a serde_json::Value, key: &str) -> std::option::Option<&'a str> {
return value.get(key).and_then(serde_json::Value::as_str);
}
fn string_array(
value: &serde_json::Value,
key: &str,
) -> std::result::Result<std::vec::Vec<std::string::String>, std::string::String> {
let values = match value.get(key).and_then(serde_json::Value::as_array) {
std::option::Option::Some(values) => values,
std::option::Option::None => {
return std::result::Result::Err(format!("Memo signer field {key} is not an array"));
},
};
let mut output = std::vec::Vec::with_capacity(values.len());
for item in values {
let text = match item.as_str() {
std::option::Option::Some(text) if !text.trim().is_empty() => text,
_ => {
return std::result::Result::Err(format!(
"Memo signer field {key} contains an invalid pubkey"
));
},
};
output.push(text.to_string());
}
return std::result::Result::Ok(output);
}
fn is_sha256(value: &str) -> bool {
return value.len() == 64
&& value
.as_bytes()
.iter()
.all(|byte| return byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase());
}
fn failed(code: &str, message: std::string::String) -> crate::MtApiMaterializerExecutionResult {
return crate::MtApiMaterializerExecutionResult {
status: crate::MtApiMaterializerOutcomeStatus::Failed,
outputs: std::vec::Vec::new(),
diagnostics: std::vec![crate::MtApiMaterializerDiagnostic {
code: code.to_string(),
message,
retriable: false,
}],
};
}
#[cfg(test)]
mod tests {
fn observation(
program_id: &str,
surface_code: &str,
generation: &str,
event_name: &str,
failed: bool,
committed: bool,
required_signers: serde_json::Value,
observed_signers: serde_json::Value,
) -> crate::DcApiDecodedObservation {
return crate::DcApiDecodedObservation {
event_key: "memo:0".to_string(),
event: crate::MdDecodedProtocolEvent {
signature: crate::MdSignature("signature".to_string()),
slot: crate::MdSlot(42),
instruction_path: crate::MdInstructionPath("1/0".to_string()),
program_id: crate::MdProgramId(program_id.to_string()),
protocol_code: crate::MdProtocolCode("spl.memo".to_string()),
surface_code: crate::MdSurfaceCode(surface_code.to_string()),
event_code: crate::MdEventCode(format!("{surface_code}.{event_name}")),
event_name: crate::MdEventName(event_name.to_string()),
event_family: crate::MdEventFamily::Audit,
source_kind: crate::MdEventSourceKind::InnerInstruction,
confidence: crate::MdDecoderConfidence::ManualExact,
},
payload_json: serde_json::json!({
"generation": generation,
"programId": program_id,
"instructionPath": "1/0",
"transactionSucceeded": !failed,
"committed": committed,
"memoText": "memo",
"payloadLengthBytes": 4,
"payloadSha256": "1111111111111111111111111111111111111111111111111111111111111111",
"payloadComplete": true,
"utf8Validation": {"status":"valid","invalidFromByte":null},
"signerValidation": {
"status": if generation == "v1" { "not_applicable" } else { "valid" },
"orderedRequiredSigners": required_signers,
"orderedObservedSigners": observed_signers,
"missingRequiredSigners": []
},
"runtimeValidation": if failed {
"not_proven_transaction_failed"
} else {
"proven_by_successful_transaction"
}
}),
transaction_failed: failed,
transaction_error: if failed {
std::option::Option::Some(serde_json::json!({"InstructionError":[0,"Custom"]}))
} else {
std::option::Option::None
},
observation_committed: committed,
proof: crate::DcApiDecoderProof {
kind: crate::DcApiDecoderProofKind::Manual,
confidence: crate::MdDecoderConfidence::ManualExact,
evidence: std::vec!["memo".to_string()],
},
};
}
fn v4() -> crate::DcApiDecodedObservation {
return observation(
kb_program_ids::SPL_MEMO_V4_PROGRAM_ID,
"spl.memo.v4",
"v4",
"add_memo",
false,
true,
serde_json::json!(["signer", "signer"]),
serde_json::json!(["signer", "signer"]),
);
}
#[test]
fn committed_v4_memo_becomes_deterministic_transaction_annotation() {
let materializer = crate::MtTransactionAnnotationMaterializer;
let first = crate::MtApiEventMaterializer::materialize(&materializer, &v4());
let second = crate::MtApiEventMaterializer::materialize(&materializer, &v4());
assert_eq!(first, second);
assert_eq!(first.status, crate::MtApiMaterializerOutcomeStatus::Inserted);
assert_eq!(first.outputs.len(), 1);
assert_eq!(
first.outputs[0].family,
crate::MdMaterializedEventFamily::TransactionAnnotation
);
assert_eq!(
first.outputs[0].payload_json["signers"]["verified"],
serde_json::json!(["signer", "signer"])
);
assert_eq!(first.outputs[0].payload_json["committed"], true);
assert!(first.validate().is_ok());
}
#[test]
fn exact_v1_v3_and_v4_surfaces_are_accepted() {
for (program_id, surface, generation) in [
(kb_program_ids::SPL_MEMO_V1_PROGRAM_ID, "spl.memo.v1", "v1"),
(kb_program_ids::SPL_MEMO_V3_PROGRAM_ID, "spl.memo.v3", "v3"),
(kb_program_ids::SPL_MEMO_V4_PROGRAM_ID, "spl.memo.v4", "v4"),
] {
let observation = observation(
program_id,
surface,
generation,
"add_memo",
false,
true,
serde_json::json!([]),
serde_json::json!([]),
);
assert!(crate::MtApiEventMaterializer::accepts_observation(
&crate::MtTransactionAnnotationMaterializer,
&observation,
));
}
}
#[test]
fn v1_observed_accounts_are_not_claimed_as_runtime_verified() {
let observation = observation(
kb_program_ids::SPL_MEMO_V1_PROGRAM_ID,
"spl.memo.v1",
"v1",
"add_memo",
false,
true,
serde_json::json!([]),
serde_json::json!(["observed_only"]),
);
let result = crate::MtApiEventMaterializer::materialize(
&crate::MtTransactionAnnotationMaterializer,
&observation,
);
assert_eq!(result.outputs[0].payload_json["signers"]["verified"], serde_json::json!([]));
assert_eq!(
result.outputs[0].payload_json["signers"]["observed"],
serde_json::json!(["observed_only"])
);
}
#[test]
fn failed_or_uncommitted_memo_is_refused_without_output() {
let observation = observation(
kb_program_ids::SPL_MEMO_V4_PROGRAM_ID,
"spl.memo.v4",
"v4",
"memo_intent",
true,
false,
serde_json::json!([]),
serde_json::json!([]),
);
let materializer = crate::MtTransactionAnnotationMaterializer;
let policy =
crate::materializer_api_validate_materialization_policy(&materializer, &observation);
assert!(policy.is_err());
let result = crate::MtApiEventMaterializer::materialize(&materializer, &observation);
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Refused);
assert!(result.outputs.is_empty());
}
#[test]
fn malformed_committed_payload_fails_closed() {
let mut observation = v4();
observation.payload_json["payloadSha256"] = serde_json::Value::String("bad".to_string());
let result = crate::MtApiEventMaterializer::materialize(
&crate::MtTransactionAnnotationMaterializer,
&observation,
);
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Failed);
assert!(result.outputs.is_empty());
assert_eq!(result.diagnostics[0].code, "memo_annotation_payload_invalid");
}
#[test]
fn foreign_audit_observation_is_not_accepted() {
let mut observation = v4();
observation.event.protocol_code = crate::MdProtocolCode("foreign".to_string());
assert!(!crate::MtApiEventMaterializer::accepts_observation(
&crate::MtTransactionAnnotationMaterializer,
&observation,
));
let result = crate::MtApiEventMaterializer::materialize(
&crate::MtTransactionAnnotationMaterializer,
&observation,
);
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Ignored);
}
#[test]
fn machine_readable_matrix_declares_transaction_annotation_policy() {
let parsed = serde_json::from_str::<serde_json::Value>(include_str!(
"../../../../test-fixtures/contract-matrices/SPL_MEMO_MATRIX.json"
));
let matrix = match parsed {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("memo matrix is invalid JSON: {error}"),
};
assert_eq!(matrix["matrixVersion"], 4);
assert_eq!(matrix["materializationContract"]["crate"], "kb-lib");
assert_eq!(
matrix["materializationContract"]["transactionPolicy"],
"SuccessfulCommittedOnly"
);
assert_eq!(matrix["materializationContract"]["failedOrUncommittedOutput"], false);
}
}
/// Crate-visible `MT_TRANSACTION_ANNOTATIONS_ACCEPTED_FAMILIES` component constant.
pub(crate) use self::constants::MT_TRANSACTION_ANNOTATIONS_ACCEPTED_FAMILIES;
/// Stable runtime component name for transaction annotations.
pub(crate) use self::constants::MT_TRANSACTION_ANNOTATIONS_COMPONENT_NAME;
/// Maximum Memo payload accepted by transaction annotations.
pub(crate) use self::constants::MT_TRANSACTION_ANNOTATIONS_MAX_MEMO_PAYLOAD_BYTES;
/// Crate-visible `MT_TRANSACTION_ANNOTATIONS_MEMO_ENTRY_NAMES` component constant.
pub(crate) use self::constants::MT_TRANSACTION_ANNOTATIONS_MEMO_ENTRY_NAMES;
/// Stable processor name for transaction annotations.
pub(crate) use self::constants::MT_TRANSACTION_ANNOTATIONS_PROCESSOR_NAME;
/// Stable projection contract version for transaction annotations.
pub(crate) use self::constants::MT_TRANSACTION_ANNOTATIONS_PROJECTION_VERSION;
/// Canonical tracing target for transaction annotations.
pub(crate) use self::constants::TRACING_TARGET_MATERIALIZER_TRANSACTION_ANNOTATIONS;