// file: kb-lib/src/materializer/transaction/annotations.rs // version: 4 //! Exact committed SPL Memo transaction annotation projection. const ACCEPTED_FAMILIES: &[crate::MdEventFamily] = &[crate::MdEventFamily::Audit]; const MEMO_ENTRY_NAMES: &[&str] = &["add_memo", "memo_intent", "invalid_memo_attempt"]; #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum MemoGeneration { V1, V3, V4, } impl MemoGeneration { fn from_event(event: &crate::MdDecodedProtocolEvent) -> std::option::Option { 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, observed_signers: std::vec::Vec, verified_signers: std::vec::Vec, 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_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> { 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 { 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::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::(include_str!( "../../../../docs/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); } }