// file: kb-lib/src/decoder/spl/memo/decoder.rs // version: 2 //! Exact SPL Memo v1, v3 and v4 dispatch for the common decode pipeline. const MEMO_SURFACES: &[crate::DcApiDecoderSurface] = &[ crate::DcApiDecoderSurface { program_id: kb_program_ids::SPL_MEMO_V1_PROGRAM_ID, surface_code: crate::DC_SPL_MEMO_V1_SURFACE_CODE, priority: 100, }, crate::DcApiDecoderSurface { program_id: kb_program_ids::SPL_MEMO_V3_PROGRAM_ID, surface_code: crate::DC_SPL_MEMO_V3_SURFACE_CODE, priority: 100, }, crate::DcApiDecoderSurface { program_id: kb_program_ids::SPL_MEMO_V4_PROGRAM_ID, surface_code: crate::DC_SPL_MEMO_V4_SURFACE_CODE, priority: 100, }, ]; const LEGACY_PROGRAM_IDS: &[&str] = &[ kb_program_ids::SPL_MEMO_V1_PROGRAM_ID, kb_program_ids::SPL_MEMO_V3_PROGRAM_ID, kb_program_ids::SPL_MEMO_V4_PROGRAM_ID, ]; /// Exact decoder for the three registered SPL Memo generations. #[derive(Clone, Debug, Default)] pub struct DcSplMemoDecoder; impl crate::DcApiProtocolDecoder for crate::DcSplMemoDecoder { fn decoder_name(&self) -> &'static str { return "kb_decoder_spl_memo"; } fn decoder_version(&self) -> &'static str { return env!("CARGO_PKG_VERSION"); } fn program_ids(&self) -> &'static [&'static str] { return LEGACY_PROGRAM_IDS; } fn supports_observation( &self, observation: &crate::MdProgramObservation, ) -> crate::DcApiDecoderSupport { if crate::DcApiProtocolDecoder::handles_program_id(self, &observation.program_id) { return crate::DcApiDecoderSupport::Yes; } return crate::DcApiDecoderSupport::No; } fn decode_observation( &self, _observation: &crate::MdProgramObservation, ) -> kb_core::Result> { return std::result::Result::Ok(std::vec::Vec::new()); } } impl crate::DcApiInstructionDecoder for crate::DcSplMemoDecoder { fn identity(&self) -> crate::DcApiDecoderIdentity { return crate::DcApiDecoderIdentity { name: "spl_memo".to_string(), version: env!("CARGO_PKG_VERSION").to_string(), }; } fn surfaces(&self) -> &'static [crate::DcApiDecoderSurface] { return MEMO_SURFACES; } fn coverage(&self) -> std::vec::Vec { return MEMO_SURFACES .iter() .map(|surface| { return crate::DcApiDecoderCoverageDeclaration { program_id: surface.program_id.to_string(), surface_code: std::option::Option::Some(surface.surface_code.to_string()), entry_kind: crate::DcApiDecoderCoverageEntryKind::Instruction, entry_code: "add_memo".to_string(), discriminator_hex: std::option::Option::None, historical: surface.program_id != kb_program_ids::SPL_MEMO_V4_PROGRAM_ID, }; }) .collect(); } fn recognize( &self, input: &crate::MdCoreInstructionReplayInput, ) -> crate::DcApiDecoderRecognition { let surface = MEMO_SURFACES .iter() .find(|surface| return surface.program_id == input.program_id); let surface = match surface { std::option::Option::Some(value) => value, std::option::Option::None => return crate::DcApiDecoderRecognition::incompatible(), }; return crate::DcApiDecoderRecognition::compatible( true, surface.priority, std::option::Option::Some(surface.surface_code.to_string()), std::option::Option::Some("add_memo".to_string()), std::option::Option::None, ); } fn decode( &self, input: &crate::MdCoreInstructionReplayInput, ) -> crate::DcApiDecoderExecutionResult { if !LEGACY_PROGRAM_IDS.contains(&input.program_id.as_str()) { return crate::DcApiDecoderExecutionResult::unsupported(std::option::Option::None); } let result = crate::decoder_spl_memo_decode(input); if matches!( result.status, crate::DcApiDecoderOutcomeStatus::Failed | crate::DcApiDecoderOutcomeStatus::Unsupported ) { tracing::error!( target: crate::TRACING_TARGET_DECODER_SPL_MEMO, action = "decode_failure", signature = %input.signature, slot = input.slot, instruction_path = %input.instruction_path, program_id = %input.program_id, processor_name = "spl_memo", processor_version = env!("CARGO_PKG_VERSION"), input_key = %input.replay_input_key, payload_hash = ?input.instruction_payload_hash, transaction_failed = input.transaction_failed, result_status = ?result.status, diagnostics = ?result.diagnostics, "SPL Memo instruction was not decoded successfully" ); } tracing::debug!( target: crate::TRACING_TARGET_DECODER_SPL_MEMO, action = "decode", signature = %input.signature, instruction_path = %input.instruction_path, program_id = %input.program_id, transaction_failed = input.transaction_failed, result_status = ?result.status, observation_count = result.observations.len(), "SPL Memo instruction decode completed" ); return result; } } #[cfg(test)] mod tests { use base64::Engine; // rust-rules: trait-import fn input( program_id: &str, payload: &[u8], accounts: serde_json::Value, keys: serde_json::Value, failed: bool, path: &str, ) -> crate::MdCoreInstructionReplayInput { let result = crate::MdCoreInstructionReplayInput::new( format!("signature:{path}"), "signature", 42, path, program_id, failed, if failed { std::option::Option::Some(serde_json::json!({"InstructionError":[0,"Custom"]})) } else { std::option::Option::None }, keys, accounts, std::option::Option::Some(serde_json::json!({ "dataBase64": base64::engine::general_purpose::STANDARD.encode(payload) })), std::option::Option::None, serde_json::json!([]), serde_json::json!([]), serde_json::json!([]), serde_json::json!([]), ); return match result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("memo replay input failed: {error}"), }; } fn one_account(signer: bool) -> (serde_json::Value, serde_json::Value) { return ( serde_json::json!([{"position":0,"accountIndex":1,"accountKey":"signer"}]), serde_json::json!([{"accountIndex":1,"accountKey":"signer","signer":signer,"writable":false,"source":"static"}]), ); } #[test] fn exact_support_and_interface_ids_cover_all_generations() { let decoder = crate::DcSplMemoDecoder; assert_eq!(spl_memo_interface::v1::ID.to_string(), kb_program_ids::SPL_MEMO_V1_PROGRAM_ID); assert_eq!(spl_memo_interface::v3::ID.to_string(), kb_program_ids::SPL_MEMO_V3_PROGRAM_ID); assert_eq!(spl_memo_interface::v4::ID.to_string(), kb_program_ids::SPL_MEMO_V4_PROGRAM_ID); assert_eq!(crate::DcApiInstructionDecoder::surfaces(&decoder).len(), 3); assert!( crate::DcApiInstructionDecoder::coverage(&decoder) .iter() .all(|entry| return entry.entry_code == "add_memo") ); } #[test] fn decodes_empty_ascii_unicode_outer_and_inner_payloads() { for (index, payload) in [b"".as_slice(), b"payment:42".as_slice(), "memo ".as_bytes()] .iter() .enumerate() { let path = if index == 2 { "1/0" } else { "0" }; let input = input( kb_program_ids::SPL_MEMO_V4_PROGRAM_ID, payload, serde_json::json!([]), serde_json::json!([]), false, path, ); let result = crate::DcApiInstructionDecoder::decode(&crate::DcSplMemoDecoder, &input); assert_eq!(result.status, crate::DcApiDecoderOutcomeStatus::Decoded); assert_eq!( result.observations[0].payload_json["payloadLengthBytes"], serde_json::json!(payload.len()) ); assert!(result.observations[0].observation_committed); assert_eq!( result.observations[0].event.source_kind, if path.contains('/') { crate::MdEventSourceKind::InnerInstruction } else { crate::MdEventSourceKind::Instruction } ); } } #[test] fn v1_ignores_accounts_while_v3_and_v4_require_every_account_to_sign() { let (accounts, keys) = one_account(false); let v1 = input( kb_program_ids::SPL_MEMO_V1_PROGRAM_ID, b"legacy", accounts.clone(), keys.clone(), false, "0", ); let v1_result = crate::DcApiInstructionDecoder::decode(&crate::DcSplMemoDecoder, &v1); assert_eq!(v1_result.observations[0].event.event_name.0, "add_memo"); assert_eq!( v1_result.observations[0].payload_json["signerValidation"]["status"], "not_applicable" ); for program_id in [kb_program_ids::SPL_MEMO_V3_PROGRAM_ID, kb_program_ids::SPL_MEMO_V4_PROGRAM_ID] { let input = input(program_id, b"signed", accounts.clone(), keys.clone(), false, "0"); let result = crate::DcApiInstructionDecoder::decode(&crate::DcSplMemoDecoder, &input); assert_eq!(result.observations[0].event.event_name.0, "invalid_memo_attempt"); assert_eq!( result.observations[0].payload_json["signerValidation"]["status"], "invalid" ); assert!(!result.observations[0].observation_committed); } } #[test] fn invalid_utf8_and_failed_transactions_are_explicit_uncommitted_intents() { let invalid = input( kb_program_ids::SPL_MEMO_V4_PROGRAM_ID, &[0xf0, 0x9f, 0x90, 0xff], serde_json::json!([]), serde_json::json!([]), true, "0", ); let invalid_result = crate::DcApiInstructionDecoder::decode(&crate::DcSplMemoDecoder, &invalid); assert_eq!(invalid_result.observations[0].event.event_name.0, "invalid_memo_attempt"); assert_eq!( invalid_result.observations[0].payload_json["utf8Validation"]["status"], "invalid" ); assert!(!invalid_result.observations[0].observation_committed); let valid_failed = input( kb_program_ids::SPL_MEMO_V3_PROGRAM_ID, b"rolled back", serde_json::json!([]), serde_json::json!([]), true, "0", ); let failed_result = crate::DcApiInstructionDecoder::decode(&crate::DcSplMemoDecoder, &valid_failed); assert_eq!(failed_result.observations[0].event.event_name.0, "memo_intent"); assert!(!failed_result.observations[0].observation_committed); assert_eq!( failed_result.observations[0].payload_json["runtimeValidation"], "not_proven_transaction_failed" ); } #[test] fn preserves_duplicate_writable_and_multiple_signer_accounts_in_order() { let accounts = serde_json::json!([ {"position":0,"accountIndex":1,"accountKey":"first"}, {"position":1,"accountIndex":2,"accountKey":"second"}, {"position":2,"accountIndex":1,"accountKey":"first"} ]); let keys = serde_json::json!([ {"accountIndex":1,"accountKey":"first","signer":true,"writable":true,"source":"static"}, {"accountIndex":2,"accountKey":"second","signer":true,"writable":false,"source":"static"} ]); let input = input(kb_program_ids::SPL_MEMO_V4_PROGRAM_ID, b"ordered", accounts, keys, false, "0"); let result = crate::DcApiInstructionDecoder::decode(&crate::DcSplMemoDecoder, &input); assert_eq!(result.observations[0].payload_json["accounts"][0]["pubkey"], "first"); assert_eq!(result.observations[0].payload_json["accounts"][0]["writable"], true); assert_eq!(result.observations[0].payload_json["accounts"][2]["pubkey"], "first"); assert_eq!( result.observations[0].payload_json["signerValidation"]["orderedRequiredSigners"], serde_json::json!(["first", "second", "first"]) ); assert_eq!( result.observations[0].payload_json["signerValidation"]["orderedObservedSigners"], serde_json::json!(["first", "second", "first"]) ); } #[test] fn payload_hash_is_canonical_sha256() { let input = input( kb_program_ids::SPL_MEMO_V4_PROGRAM_ID, b"abc", serde_json::json!([]), serde_json::json!([]), false, "0", ); let result = crate::DcApiInstructionDecoder::decode(&crate::DcSplMemoDecoder, &input); assert_eq!( result.observations[0].payload_json["payloadSha256"], "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" ); } #[test] fn payload_bound_accepts_limit_and_rejects_oversize_and_malformed_base64() { let at_limit = std::vec![b'x'; crate::DC_SPL_MEMO_MAX_PAYLOAD_BYTES]; let cinput = input( kb_program_ids::SPL_MEMO_V4_PROGRAM_ID, at_limit.as_slice(), serde_json::json!([]), serde_json::json!([]), false, "0", ); let result = crate::DcApiInstructionDecoder::decode(&crate::DcSplMemoDecoder, &cinput); assert_eq!(result.status, crate::DcApiDecoderOutcomeStatus::Decoded); let oversize = std::vec![b'x'; crate::DC_SPL_MEMO_MAX_PAYLOAD_BYTES + 1]; let cinput = input( kb_program_ids::SPL_MEMO_V4_PROGRAM_ID, oversize.as_slice(), serde_json::json!([]), serde_json::json!([]), false, "0", ); let result = crate::DcApiInstructionDecoder::decode(&crate::DcSplMemoDecoder, &cinput); assert_eq!(result.status, crate::DcApiDecoderOutcomeStatus::Failed); let mut malformed = input( kb_program_ids::SPL_MEMO_V4_PROGRAM_ID, b"valid", serde_json::json!([]), serde_json::json!([]), false, "0", ); malformed.instruction_payload_json = std::option::Option::Some(serde_json::json!({"dataBase64":"%%%"})); let result = crate::DcApiInstructionDecoder::decode(&crate::DcSplMemoDecoder, &malformed); assert_eq!(result.status, crate::DcApiDecoderOutcomeStatus::Failed); assert!(result.observations.is_empty()); } #[test] fn unknown_program_is_incompatible_and_never_emits_an_observation() { let input = input( kb_program_ids::SYSTEM_PROGRAM_ID, b"not memo", serde_json::json!([]), serde_json::json!([]), false, "0", ); let recognition = crate::DcApiInstructionDecoder::recognize(&crate::DcSplMemoDecoder, &input); assert!(!recognition.compatible); let result = crate::DcApiInstructionDecoder::decode(&crate::DcSplMemoDecoder, &input); assert_eq!(result.status, crate::DcApiDecoderOutcomeStatus::Unsupported); assert!(result.observations.is_empty()); } #[test] fn matrix_is_machine_readable_and_covers_required_cases() { 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["generations"].as_array().map(std::vec::Vec::len), std::option::Option::Some(3) ); let cases = matrix["requiredCorpusCases"].as_array(); assert!(cases.is_some_and(|values| return values.len() >= 12)); } }