0.1.0-pre.005
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// file: kb-lib/src/decoder.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Consolidated decoder modules.
|
||||
|
||||
@@ -32,3 +32,25 @@ pub mod vault;
|
||||
pub mod vesting;
|
||||
pub mod wallet;
|
||||
pub mod weighted;
|
||||
|
||||
/// Maximum payload prefix retained for bounded diagnostics.
|
||||
pub(crate) use self::spl::SPL_MEMO_DIAGNOSTIC_PREFIX_BYTES;
|
||||
/// Current stable Memo decoded event contract version.
|
||||
pub(crate) use self::spl::SPL_MEMO_EVENT_VERSION;
|
||||
/// Maximum decoded instruction data retained as a complete Memo payload.
|
||||
pub(crate) use self::spl::SPL_MEMO_MAX_PAYLOAD_BYTES;
|
||||
/// Stable protocol code shared by Memo generations.
|
||||
pub(crate) use self::spl::SPL_MEMO_PROTOCOL_CODE;
|
||||
/// Canonical tracing target for the SPL Memo decoder.
|
||||
pub(crate) use self::spl::SPL_MEMO_TRACING_TARGET;
|
||||
/// Stable Memo v1 surface code.
|
||||
pub(crate) use self::spl::SPL_MEMO_V1_SURFACE_CODE;
|
||||
/// Stable Memo v3 surface code.
|
||||
pub(crate) use self::spl::SPL_MEMO_V3_SURFACE_CODE;
|
||||
/// Stable Memo v4 surface code.
|
||||
pub(crate) use self::spl::SPL_MEMO_V4_SURFACE_CODE;
|
||||
/// Decodes one bounded SPL Memo instruction.
|
||||
pub(crate) use self::spl::spl_memo_decode;
|
||||
|
||||
/// Exact decoder for the three registered SPL Memo generations.
|
||||
pub use self::spl::SplMemoDecoder;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-lib/src/decoder/spl.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! `spl` decoder family.
|
||||
|
||||
@@ -12,3 +12,25 @@ pub mod single_pool;
|
||||
pub mod stake_pool;
|
||||
pub mod token;
|
||||
pub mod token_2022;
|
||||
|
||||
/// Maximum payload prefix retained for bounded diagnostics.
|
||||
pub(crate) use self::memo::SPL_MEMO_DIAGNOSTIC_PREFIX_BYTES;
|
||||
/// Current stable Memo decoded event contract version.
|
||||
pub(crate) use self::memo::SPL_MEMO_EVENT_VERSION;
|
||||
/// Maximum decoded instruction data retained as a complete Memo payload.
|
||||
pub(crate) use self::memo::SPL_MEMO_MAX_PAYLOAD_BYTES;
|
||||
/// Stable protocol code shared by Memo generations.
|
||||
pub(crate) use self::memo::SPL_MEMO_PROTOCOL_CODE;
|
||||
/// Canonical tracing target for the SPL Memo decoder.
|
||||
pub(crate) use self::memo::SPL_MEMO_TRACING_TARGET;
|
||||
/// Stable Memo v1 surface code.
|
||||
pub(crate) use self::memo::SPL_MEMO_V1_SURFACE_CODE;
|
||||
/// Stable Memo v3 surface code.
|
||||
pub(crate) use self::memo::SPL_MEMO_V3_SURFACE_CODE;
|
||||
/// Stable Memo v4 surface code.
|
||||
pub(crate) use self::memo::SPL_MEMO_V4_SURFACE_CODE;
|
||||
/// Decodes one bounded SPL Memo instruction.
|
||||
pub(crate) use self::memo::spl_memo_decode;
|
||||
|
||||
/// Exact decoder for the three registered SPL Memo generations.
|
||||
pub use self::memo::SplMemoDecoder;
|
||||
|
||||
@@ -1,10 +1,30 @@
|
||||
// file: kb-lib/src/decoder/spl/memo.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Migration boundary for legacy crate `kb_decoder_spl_memo`.
|
||||
//! Exact SPL Memo v1, v3 and v4 decoder component.
|
||||
|
||||
/// Legacy crate name retained for migration and compatibility tracking.
|
||||
pub const LEGACY_CRATE: &str = "kb_decoder_spl_memo";
|
||||
mod constants;
|
||||
mod decoder;
|
||||
mod payload;
|
||||
|
||||
/// Current porting status.
|
||||
pub const MIGRATION_STATUS: &str = "source-preserved-pending-port";
|
||||
/// Maximum payload prefix retained for bounded diagnostics.
|
||||
pub(crate) use self::constants::SPL_MEMO_DIAGNOSTIC_PREFIX_BYTES;
|
||||
/// Current stable Memo decoded event contract version.
|
||||
pub(crate) use self::constants::SPL_MEMO_EVENT_VERSION;
|
||||
/// Maximum decoded instruction data retained as a complete Memo payload.
|
||||
pub(crate) use self::constants::SPL_MEMO_MAX_PAYLOAD_BYTES;
|
||||
/// Stable protocol code shared by Memo generations.
|
||||
pub(crate) use self::constants::SPL_MEMO_PROTOCOL_CODE;
|
||||
/// Canonical tracing target for the SPL Memo decoder.
|
||||
pub(crate) use self::constants::SPL_MEMO_TRACING_TARGET;
|
||||
/// Stable Memo v1 surface code.
|
||||
pub(crate) use self::constants::SPL_MEMO_V1_SURFACE_CODE;
|
||||
/// Stable Memo v3 surface code.
|
||||
pub(crate) use self::constants::SPL_MEMO_V3_SURFACE_CODE;
|
||||
/// Stable Memo v4 surface code.
|
||||
pub(crate) use self::constants::SPL_MEMO_V4_SURFACE_CODE;
|
||||
/// Decodes one bounded SPL Memo instruction.
|
||||
pub(crate) use self::payload::decode as spl_memo_decode;
|
||||
|
||||
/// Exact decoder for the three registered SPL Memo generations.
|
||||
pub use self::decoder::SplMemoDecoder;
|
||||
|
||||
21
kb-lib/src/decoder/spl/memo/constants.rs
Normal file
21
kb-lib/src/decoder/spl/memo/constants.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
// file: kb-lib/src/decoder/spl/memo/constants.rs
|
||||
// version: 2
|
||||
|
||||
//! Local constants for the `kb-lib` SPL Memo component. Program identifiers live in `kb_program_ids`.
|
||||
|
||||
/// Stable protocol code shared by Memo generations.
|
||||
pub(crate) const SPL_MEMO_PROTOCOL_CODE: &str = "spl_memo";
|
||||
/// Stable Memo v1 surface code.
|
||||
pub(crate) const SPL_MEMO_V1_SURFACE_CODE: &str = "spl_memo_v1";
|
||||
/// Stable Memo v3 surface code.
|
||||
pub(crate) const SPL_MEMO_V3_SURFACE_CODE: &str = "spl_memo_v3";
|
||||
/// Stable Memo v4 surface code.
|
||||
pub(crate) const SPL_MEMO_V4_SURFACE_CODE: &str = "spl_memo_v4";
|
||||
/// Maximum decoded instruction data retained as a complete Memo payload.
|
||||
pub(crate) const SPL_MEMO_MAX_PAYLOAD_BYTES: usize = 4_096;
|
||||
/// Maximum payload prefix retained for bounded diagnostics.
|
||||
pub(crate) const SPL_MEMO_DIAGNOSTIC_PREFIX_BYTES: usize = 32;
|
||||
/// Current stable Memo decoded event contract version.
|
||||
pub(crate) const SPL_MEMO_EVENT_VERSION: u32 = 1;
|
||||
/// Canonical tracing target for this crate.
|
||||
pub(crate) const SPL_MEMO_TRACING_TARGET: &str = "kb-lib.decoder.spl.memo";
|
||||
429
kb-lib/src/decoder/spl/memo/decoder.rs
Normal file
429
kb-lib/src/decoder/spl/memo/decoder.rs
Normal file
@@ -0,0 +1,429 @@
|
||||
// file: kb-lib/src/decoder/spl/memo/decoder.rs
|
||||
// version: 1
|
||||
|
||||
//! Exact SPL Memo v1, v3 and v4 dispatch for the common decode pipeline.
|
||||
|
||||
const MEMO_SURFACES: &[crate::DecoderSurface] = &[
|
||||
crate::DecoderSurface {
|
||||
program_id: kb_program_ids::SPL_MEMO_V1_PROGRAM_ID,
|
||||
surface_code: crate::SPL_MEMO_V1_SURFACE_CODE,
|
||||
priority: 100,
|
||||
},
|
||||
crate::DecoderSurface {
|
||||
program_id: kb_program_ids::SPL_MEMO_V3_PROGRAM_ID,
|
||||
surface_code: crate::SPL_MEMO_V3_SURFACE_CODE,
|
||||
priority: 100,
|
||||
},
|
||||
crate::DecoderSurface {
|
||||
program_id: kb_program_ids::SPL_MEMO_V4_PROGRAM_ID,
|
||||
surface_code: crate::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 SplMemoDecoder;
|
||||
|
||||
impl crate::ProtocolDecoder for crate::SplMemoDecoder {
|
||||
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::ProgramObservation,
|
||||
) -> crate::DecoderSupport {
|
||||
if crate::ProtocolDecoder::handles_program_id(self, &observation.program_id) {
|
||||
return crate::DecoderSupport::Yes;
|
||||
}
|
||||
return crate::DecoderSupport::No;
|
||||
}
|
||||
|
||||
fn decode_observation(
|
||||
&self,
|
||||
_observation: &crate::ProgramObservation,
|
||||
) -> kb_core::Result<std::vec::Vec<crate::DecodedProtocolEvent>> {
|
||||
return std::result::Result::Ok(std::vec::Vec::new());
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::InstructionDecoder for crate::SplMemoDecoder {
|
||||
fn identity(&self) -> crate::DecoderIdentity {
|
||||
return crate::DecoderIdentity {
|
||||
name: "spl_memo".to_string(),
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
fn surfaces(&self) -> &'static [crate::DecoderSurface] {
|
||||
return MEMO_SURFACES;
|
||||
}
|
||||
|
||||
fn coverage(&self) -> std::vec::Vec<crate::DecoderCoverageDeclaration> {
|
||||
return MEMO_SURFACES
|
||||
.iter()
|
||||
.map(|surface| {
|
||||
return crate::DecoderCoverageDeclaration {
|
||||
program_id: surface.program_id.to_string(),
|
||||
surface_code: std::option::Option::Some(surface.surface_code.to_string()),
|
||||
entry_kind: crate::DecoderCoverageEntryKind::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::CoreInstructionReplayInput) -> crate::DecoderRecognition {
|
||||
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::DecoderRecognition::incompatible(),
|
||||
};
|
||||
return crate::DecoderRecognition::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::CoreInstructionReplayInput) -> crate::DecoderExecutionResult {
|
||||
if !LEGACY_PROGRAM_IDS.contains(&input.program_id.as_str()) {
|
||||
return crate::DecoderExecutionResult::unsupported(std::option::Option::None);
|
||||
}
|
||||
let result = crate::spl_memo_decode(input);
|
||||
if matches!(
|
||||
result.status,
|
||||
crate::DecoderOutcomeStatus::Failed | crate::DecoderOutcomeStatus::Unsupported
|
||||
) {
|
||||
tracing::error!(
|
||||
target: crate::SPL_MEMO_TRACING_TARGET,
|
||||
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::SPL_MEMO_TRACING_TARGET,
|
||||
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::CoreInstructionReplayInput {
|
||||
let result = crate::CoreInstructionReplayInput::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::SplMemoDecoder;
|
||||
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::InstructionDecoder::surfaces(&decoder).len(), 3);
|
||||
assert!(
|
||||
crate::InstructionDecoder::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::InstructionDecoder::decode(&crate::SplMemoDecoder, &input);
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::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::EventSourceKind::InnerInstruction
|
||||
} else {
|
||||
crate::EventSourceKind::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::InstructionDecoder::decode(&crate::SplMemoDecoder, &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::InstructionDecoder::decode(&crate::SplMemoDecoder, &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::InstructionDecoder::decode(&crate::SplMemoDecoder, &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::InstructionDecoder::decode(&crate::SplMemoDecoder, &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::InstructionDecoder::decode(&crate::SplMemoDecoder, &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::InstructionDecoder::decode(&crate::SplMemoDecoder, &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::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::InstructionDecoder::decode(&crate::SplMemoDecoder, &cinput);
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
let oversize = std::vec![b'x'; crate::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::InstructionDecoder::decode(&crate::SplMemoDecoder, &cinput);
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::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::InstructionDecoder::decode(&crate::SplMemoDecoder, &malformed);
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::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::InstructionDecoder::recognize(&crate::SplMemoDecoder, &input);
|
||||
assert!(!recognition.compatible);
|
||||
let result = crate::InstructionDecoder::decode(&crate::SplMemoDecoder, &input);
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Unsupported);
|
||||
assert!(result.observations.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matrix_is_machine_readable_and_covers_required_cases() {
|
||||
let parsed = serde_json::from_str::<serde_json::Value>(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));
|
||||
}
|
||||
}
|
||||
371
kb-lib/src/decoder/spl/memo/payload.rs
Normal file
371
kb-lib/src/decoder/spl/memo/payload.rs
Normal file
@@ -0,0 +1,371 @@
|
||||
// file: kb-lib/src/decoder/spl/memo/payload.rs
|
||||
// version: 1
|
||||
|
||||
//! Bounded Memo payload, account and validation projection.
|
||||
|
||||
use base64::Engine; // rust-rules: trait-import
|
||||
use sha2::Digest; // rust-rules: trait-import
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum MemoGeneration {
|
||||
V1,
|
||||
V3,
|
||||
V4,
|
||||
}
|
||||
|
||||
impl MemoGeneration {
|
||||
fn from_program_id(program_id: &str) -> std::option::Option<Self> {
|
||||
if program_id == kb_program_ids::SPL_MEMO_V1_PROGRAM_ID {
|
||||
return std::option::Option::Some(Self::V1);
|
||||
}
|
||||
if program_id == kb_program_ids::SPL_MEMO_V3_PROGRAM_ID {
|
||||
return std::option::Option::Some(Self::V3);
|
||||
}
|
||||
if program_id == kb_program_ids::SPL_MEMO_V4_PROGRAM_ID {
|
||||
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 surface_code(self) -> &'static str {
|
||||
return match self {
|
||||
Self::V1 => crate::SPL_MEMO_V1_SURFACE_CODE,
|
||||
Self::V3 => crate::SPL_MEMO_V3_SURFACE_CODE,
|
||||
Self::V4 => crate::SPL_MEMO_V4_SURFACE_CODE,
|
||||
};
|
||||
}
|
||||
|
||||
fn validates_signers(self) -> bool {
|
||||
return self != Self::V1;
|
||||
}
|
||||
}
|
||||
|
||||
struct ResolvedAccounts {
|
||||
values: serde_json::Value,
|
||||
ordered_accounts: std::vec::Vec<std::string::String>,
|
||||
ordered_signers: std::vec::Vec<std::string::String>,
|
||||
missing_signers: std::vec::Vec<std::string::String>,
|
||||
}
|
||||
|
||||
pub(crate) fn decode(input: &crate::CoreInstructionReplayInput) -> crate::DecoderExecutionResult {
|
||||
let generation = match MemoGeneration::from_program_id(input.program_id.as_str()) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return crate::DecoderExecutionResult::unsupported(std::option::Option::None);
|
||||
},
|
||||
};
|
||||
let bytes = match decode_payload(input) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return failed("memo_payload_unavailable", error.to_string());
|
||||
},
|
||||
};
|
||||
let accounts = match resolve_accounts(input) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return failed("memo_accounts_malformed", error.to_string());
|
||||
},
|
||||
};
|
||||
let text_result = std::str::from_utf8(bytes.as_slice());
|
||||
let utf8_valid = text_result.is_ok();
|
||||
let signer_valid = !generation.validates_signers() || accounts.missing_signers.is_empty();
|
||||
let structurally_valid = utf8_valid && signer_valid;
|
||||
let event_name = if !structurally_valid {
|
||||
"invalid_memo_attempt"
|
||||
} else if input.transaction_failed {
|
||||
"memo_intent"
|
||||
} else {
|
||||
"add_memo"
|
||||
};
|
||||
let memo_text = match text_result {
|
||||
std::result::Result::Ok(value) => serde_json::Value::String(value.to_string()),
|
||||
std::result::Result::Err(_error) => serde_json::Value::Null,
|
||||
};
|
||||
let utf8_error_offset = std::str::from_utf8(bytes.as_slice())
|
||||
.err()
|
||||
.map(|error| return error.valid_up_to());
|
||||
let signer_status = if !generation.validates_signers() {
|
||||
"not_applicable"
|
||||
} else if signer_valid {
|
||||
"valid"
|
||||
} else {
|
||||
"invalid"
|
||||
};
|
||||
let runtime_validation = if input.transaction_failed {
|
||||
"not_proven_transaction_failed"
|
||||
} else if structurally_valid {
|
||||
"proven_by_successful_transaction"
|
||||
} else {
|
||||
"inconsistent_with_successful_transaction"
|
||||
};
|
||||
let missing_required_signers = if generation.validates_signers() {
|
||||
accounts.missing_signers.clone()
|
||||
} else {
|
||||
std::vec::Vec::new()
|
||||
};
|
||||
let ordered_required_signers = if generation.validates_signers() {
|
||||
accounts.ordered_accounts.clone()
|
||||
} else {
|
||||
std::vec::Vec::new()
|
||||
};
|
||||
let payload_hash = hash(bytes.as_slice());
|
||||
let event = crate::DecodedProtocolEvent {
|
||||
signature: crate::Signature(input.signature.clone()),
|
||||
slot: crate::Slot(input.slot),
|
||||
instruction_path: crate::InstructionPath(input.instruction_path.clone()),
|
||||
program_id: crate::ProgramId(input.program_id.clone()),
|
||||
protocol_code: crate::ProtocolCode(crate::SPL_MEMO_PROTOCOL_CODE.to_string()),
|
||||
surface_code: crate::SurfaceCode(generation.surface_code().to_string()),
|
||||
event_code: crate::EventCode(format!("{}.{event_name}", generation.surface_code())),
|
||||
event_name: crate::EventName(event_name.to_string()),
|
||||
event_family: crate::EventFamily::Audit,
|
||||
source_kind: if input.instruction_path.contains('/') {
|
||||
crate::EventSourceKind::InnerInstruction
|
||||
} else {
|
||||
crate::EventSourceKind::Instruction
|
||||
},
|
||||
confidence: crate::DecoderConfidence::ManualExact,
|
||||
};
|
||||
let observation = crate::DecodedObservation {
|
||||
event_key: "memo:0".to_string(),
|
||||
event,
|
||||
payload_json: serde_json::json!({
|
||||
"eventVersion": crate::SPL_MEMO_EVENT_VERSION,
|
||||
"generation": generation.code(),
|
||||
"programId": input.program_id,
|
||||
"instructionPath": input.instruction_path,
|
||||
"instructionLocation": if input.instruction_path.contains('/') { "inner" } else { "outer" },
|
||||
"transactionSucceeded": !input.transaction_failed,
|
||||
"committed": !input.transaction_failed && structurally_valid,
|
||||
"memoText": memo_text,
|
||||
"payloadLengthBytes": bytes.len(),
|
||||
"payloadSha256": payload_hash,
|
||||
"payloadHexPrefix": hex_prefix(bytes.as_slice()),
|
||||
"payloadComplete": true,
|
||||
"utf8Validation": {
|
||||
"status": if utf8_valid { "valid" } else { "invalid" },
|
||||
"invalidFromByte": utf8_error_offset
|
||||
},
|
||||
"signerValidation": {
|
||||
"requiredForGeneration": generation.validates_signers(),
|
||||
"accountsIgnoredByRuntime": !generation.validates_signers(),
|
||||
"status": signer_status,
|
||||
"orderedRequiredSigners": ordered_required_signers,
|
||||
"orderedObservedSigners": accounts.ordered_signers,
|
||||
"missingRequiredSigners": missing_required_signers
|
||||
},
|
||||
"runtimeValidation": runtime_validation,
|
||||
"accounts": accounts.values,
|
||||
"diagnostic": if structurally_valid {
|
||||
serde_json::Value::Null
|
||||
} else {
|
||||
serde_json::json!({
|
||||
"code": if !utf8_valid { "invalid_utf8" } else { "missing_required_signature" },
|
||||
"payloadHexPrefix": hex_prefix(bytes.as_slice())
|
||||
})
|
||||
}
|
||||
}),
|
||||
transaction_failed: input.transaction_failed,
|
||||
transaction_error: input.transaction_err_json.clone(),
|
||||
observation_committed: !input.transaction_failed && structurally_valid,
|
||||
proof: crate::DecoderProof {
|
||||
kind: crate::DecoderProofKind::Manual,
|
||||
confidence: crate::DecoderConfidence::ManualExact,
|
||||
evidence: std::vec![
|
||||
"spl_memo_exact_raw_instruction_data".to_string(),
|
||||
format!("payload_sha256:{payload_hash}"),
|
||||
format!("generation:{}", generation.code()),
|
||||
],
|
||||
},
|
||||
};
|
||||
return crate::DecoderExecutionResult {
|
||||
status: crate::DecoderOutcomeStatus::Decoded,
|
||||
recognized_entry_code: std::option::Option::Some("add_memo".to_string()),
|
||||
observations: std::vec![observation],
|
||||
diagnostics: std::vec::Vec::new(),
|
||||
};
|
||||
}
|
||||
|
||||
fn decode_payload(input: &crate::CoreInstructionReplayInput) -> kb_core::Result<std::vec::Vec<u8>> {
|
||||
let payload = match input.instruction_payload_json.as_ref() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(
|
||||
"Memo instruction payload is not retained",
|
||||
));
|
||||
},
|
||||
};
|
||||
let encoded = match payload.get("dataBase64").and_then(serde_json::Value::as_str) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(
|
||||
"Memo instruction payload does not contain dataBase64",
|
||||
));
|
||||
},
|
||||
};
|
||||
let maximum_encoded = crate::SPL_MEMO_MAX_PAYLOAD_BYTES
|
||||
.saturating_mul(4)
|
||||
.saturating_div(3)
|
||||
.saturating_add(4);
|
||||
if encoded.len() > maximum_encoded {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||
"Memo base64 payload exceeds {maximum_encoded} bytes"
|
||||
)));
|
||||
}
|
||||
let decoded = match base64::engine::general_purpose::STANDARD.decode(encoded.as_bytes()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||
"Memo payload is not valid base64: {error}"
|
||||
)));
|
||||
},
|
||||
};
|
||||
if decoded.len() > crate::SPL_MEMO_MAX_PAYLOAD_BYTES {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||
"Memo payload exceeds {} decoded bytes",
|
||||
crate::SPL_MEMO_MAX_PAYLOAD_BYTES
|
||||
)));
|
||||
}
|
||||
return std::result::Result::Ok(decoded);
|
||||
}
|
||||
|
||||
fn resolve_accounts(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
) -> kb_core::Result<ResolvedAccounts> {
|
||||
let instruction_accounts = match input.instruction_accounts_json.as_array() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(
|
||||
"Memo instruction accounts must be a JSON array",
|
||||
));
|
||||
},
|
||||
};
|
||||
let account_keys = match input.account_keys_json.as_array() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(
|
||||
"Memo transaction account keys must be a JSON array",
|
||||
));
|
||||
},
|
||||
};
|
||||
let mut values = std::vec::Vec::with_capacity(instruction_accounts.len());
|
||||
let mut ordered_accounts = std::vec::Vec::with_capacity(instruction_accounts.len());
|
||||
let mut ordered_signers = std::vec::Vec::new();
|
||||
let mut missing_signers = std::vec::Vec::new();
|
||||
for (position, account) in instruction_accounts.iter().enumerate() {
|
||||
let account_index = match account.get("accountIndex").and_then(serde_json::Value::as_u64) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||
"Memo instruction account {position} has no accountIndex"
|
||||
)));
|
||||
},
|
||||
};
|
||||
let account_key = match account.get("accountKey").and_then(serde_json::Value::as_str) {
|
||||
std::option::Option::Some(value) if !value.trim().is_empty() => value,
|
||||
_ => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||
"Memo instruction account {position} has no accountKey"
|
||||
)));
|
||||
},
|
||||
};
|
||||
let resolved = account_keys.iter().find(|candidate| {
|
||||
return candidate.get("accountIndex").and_then(serde_json::Value::as_u64)
|
||||
== std::option::Option::Some(account_index);
|
||||
});
|
||||
let resolved = match resolved {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||
"Memo account index {account_index} is absent from resolved keys"
|
||||
)));
|
||||
},
|
||||
};
|
||||
if resolved.get("accountKey").and_then(serde_json::Value::as_str)
|
||||
!= std::option::Option::Some(account_key)
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||
"Memo account index {account_index} resolves to a different key"
|
||||
)));
|
||||
}
|
||||
let signer = match resolved.get("signer").and_then(serde_json::Value::as_bool) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||
"Memo account index {account_index} has no signer flag"
|
||||
)));
|
||||
},
|
||||
};
|
||||
let writable = match resolved.get("writable").and_then(serde_json::Value::as_bool) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||
"Memo account index {account_index} has no writable flag"
|
||||
)));
|
||||
},
|
||||
};
|
||||
ordered_accounts.push(account_key.to_string());
|
||||
if signer {
|
||||
ordered_signers.push(account_key.to_string());
|
||||
} else {
|
||||
missing_signers.push(account_key.to_string());
|
||||
}
|
||||
let source = match resolved.get("source") {
|
||||
std::option::Option::Some(value) => value.clone(),
|
||||
std::option::Option::None => serde_json::Value::Null,
|
||||
};
|
||||
values.push(serde_json::json!({
|
||||
"position": position,
|
||||
"accountIndex": account_index,
|
||||
"pubkey": account_key,
|
||||
"signer": signer,
|
||||
"writable": writable,
|
||||
"source": source
|
||||
}));
|
||||
}
|
||||
return std::result::Result::Ok(ResolvedAccounts {
|
||||
values: serde_json::Value::Array(values),
|
||||
ordered_accounts,
|
||||
ordered_signers,
|
||||
missing_signers,
|
||||
});
|
||||
}
|
||||
|
||||
fn hash(bytes: &[u8]) -> std::string::String {
|
||||
let digest = sha2::Sha256::digest(bytes);
|
||||
let mut output = std::string::String::with_capacity(64);
|
||||
for byte in digest {
|
||||
output.push_str(format!("{byte:02x}").as_str());
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
fn hex_prefix(bytes: &[u8]) -> std::string::String {
|
||||
let length = std::cmp::min(bytes.len(), crate::SPL_MEMO_DIAGNOSTIC_PREFIX_BYTES);
|
||||
let mut output = std::string::String::with_capacity(length.saturating_mul(2));
|
||||
for byte in &bytes[..length] {
|
||||
output.push_str(format!("{byte:02x}").as_str());
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
fn failed(code: &str, message: std::string::String) -> crate::DecoderExecutionResult {
|
||||
return crate::DecoderExecutionResult {
|
||||
status: crate::DecoderOutcomeStatus::Failed,
|
||||
recognized_entry_code: std::option::Option::Some("add_memo".to_string()),
|
||||
observations: std::vec::Vec::new(),
|
||||
diagnostics: std::vec![crate::DecoderDiagnostic {
|
||||
code: code.to_string(),
|
||||
message,
|
||||
retriable: false,
|
||||
}],
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-lib/src/lib.rs
|
||||
// version: 6
|
||||
// version: 7
|
||||
|
||||
//! Consolidated decoder, executor, materializer and shared model library.
|
||||
#![warn(missing_docs)]
|
||||
@@ -11,6 +11,22 @@ pub mod executor;
|
||||
pub mod materializer;
|
||||
pub mod model;
|
||||
|
||||
/// Maximum payload prefix retained for bounded Memo diagnostics.
|
||||
pub(crate) use crate::decoder::SPL_MEMO_DIAGNOSTIC_PREFIX_BYTES;
|
||||
/// Current stable Memo decoded event contract version.
|
||||
pub(crate) use crate::decoder::SPL_MEMO_EVENT_VERSION;
|
||||
/// Maximum decoded instruction data retained as a complete Memo payload.
|
||||
pub(crate) use crate::decoder::SPL_MEMO_MAX_PAYLOAD_BYTES;
|
||||
/// Stable protocol code shared by Memo generations.
|
||||
pub(crate) use crate::decoder::SPL_MEMO_PROTOCOL_CODE;
|
||||
/// Canonical tracing target for the SPL Memo decoder.
|
||||
pub(crate) use crate::decoder::SPL_MEMO_TRACING_TARGET;
|
||||
/// Stable Memo v1 surface code.
|
||||
pub(crate) use crate::decoder::SPL_MEMO_V1_SURFACE_CODE;
|
||||
/// Stable Memo v3 surface code.
|
||||
pub(crate) use crate::decoder::SPL_MEMO_V3_SURFACE_CODE;
|
||||
/// Stable Memo v4 surface code.
|
||||
pub(crate) use crate::decoder::SPL_MEMO_V4_SURFACE_CODE;
|
||||
/// Stable Address Lookup Table surface code.
|
||||
pub(crate) use crate::decoder::solana::SOLANA_CORE_ADDRESS_LOOKUP_TABLE_SURFACE_CODE;
|
||||
/// Stable deprecated immutable BPF Loader surface code.
|
||||
@@ -61,6 +77,8 @@ pub(crate) use crate::decoder::solana::solana_core_config_decode;
|
||||
pub(crate) use crate::decoder::solana::solana_core_config_recognize;
|
||||
/// Resolves positional instruction accounts and validates their core indexes.
|
||||
pub(crate) use crate::decoder::solana::solana_core_resolve_accounts;
|
||||
/// Decodes one bounded SPL Memo instruction.
|
||||
pub(crate) use crate::decoder::spl_memo_decode;
|
||||
|
||||
/// Stable protocol code shared by native Solana events.
|
||||
pub(crate) use crate::decoder::solana::SOLANA_CORE_PROTOCOL_CODE;
|
||||
@@ -184,6 +202,8 @@ pub(crate) use crate::decoder::solana::solana_core_zk_token_proof_decode;
|
||||
/// Recognizes one historical ZK Token Proof layout or the current no-op runtime fallback.
|
||||
pub(crate) use crate::decoder::solana::solana_core_zk_token_proof_recognize;
|
||||
|
||||
/// Exact decoder for the three registered SPL Memo generations.
|
||||
pub use crate::decoder::SplMemoDecoder;
|
||||
/// Current contextual core instruction input contract version.
|
||||
pub use crate::decoder::api::contracts::CORE_INSTRUCTION_INPUT_CONTRACT_VERSION;
|
||||
/// Stable contextual decoded observation.
|
||||
|
||||
Reference in New Issue
Block a user