v0.1.0-pre.006
This commit is contained in:
500
kb-lib/src/decoder/spl/token/decoder.rs
Normal file
500
kb-lib/src/decoder/spl/token/decoder.rs
Normal file
@@ -0,0 +1,500 @@
|
||||
// file: kb-lib/src/decoder/spl/token/decoder.rs
|
||||
// version: 1
|
||||
|
||||
//! Exact classic SPL Token dispatch for the common decode pipeline.
|
||||
|
||||
const TOKEN_SURFACES: &[crate::DecoderSurface] = &[crate::DecoderSurface {
|
||||
program_id: kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
surface_code: crate::SPL_TOKEN_SURFACE_CODE,
|
||||
priority: 100,
|
||||
}];
|
||||
|
||||
const PROGRAM_IDS: &[&str] = &[kb_program_ids::SPL_TOKEN_PROGRAM_ID];
|
||||
|
||||
/// Exact decoder for the classic SPL Token program.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct SplTokenDecoder;
|
||||
|
||||
impl crate::ProtocolDecoder for crate::SplTokenDecoder {
|
||||
fn decoder_name(&self) -> &'static str {
|
||||
return "kb_decoder_spl_token";
|
||||
}
|
||||
|
||||
fn decoder_version(&self) -> &'static str {
|
||||
return env!("CARGO_PKG_VERSION");
|
||||
}
|
||||
|
||||
fn program_ids(&self) -> &'static [&'static str] {
|
||||
return PROGRAM_IDS;
|
||||
}
|
||||
|
||||
fn supports_observation(
|
||||
&self,
|
||||
observation: &crate::ProgramObservation,
|
||||
) -> crate::DecoderSupport {
|
||||
return if crate::ProtocolDecoder::handles_program_id(self, &observation.program_id) {
|
||||
crate::DecoderSupport::Yes
|
||||
} else {
|
||||
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::SplTokenDecoder {
|
||||
fn identity(&self) -> crate::DecoderIdentity {
|
||||
return crate::DecoderIdentity {
|
||||
name: "spl_token".to_string(),
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
fn surfaces(&self) -> &'static [crate::DecoderSurface] {
|
||||
return TOKEN_SURFACES;
|
||||
}
|
||||
|
||||
fn coverage(&self) -> std::vec::Vec<crate::DecoderCoverageDeclaration> {
|
||||
return crate::SPL_TOKEN_INSTRUCTION_ENTRIES
|
||||
.iter()
|
||||
.map(|(tag, code, historical)| {
|
||||
return crate::DecoderCoverageDeclaration {
|
||||
program_id: kb_program_ids::SPL_TOKEN_PROGRAM_ID.to_string(),
|
||||
surface_code: std::option::Option::Some(
|
||||
crate::SPL_TOKEN_SURFACE_CODE.to_string(),
|
||||
),
|
||||
entry_kind: crate::DecoderCoverageEntryKind::Instruction,
|
||||
entry_code: (*code).to_string(),
|
||||
discriminator_hex: std::option::Option::Some(format!("{tag:02x}")),
|
||||
historical: *historical,
|
||||
};
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
fn recognize(&self, input: &crate::CoreInstructionReplayInput) -> crate::DecoderRecognition {
|
||||
if input.program_id != kb_program_ids::SPL_TOKEN_PROGRAM_ID {
|
||||
return crate::DecoderRecognition::incompatible();
|
||||
}
|
||||
let tag = crate::spl_token_payload_tag(input);
|
||||
let entry = tag.and_then(crate::spl_token_entry_for_tag);
|
||||
return crate::DecoderRecognition::compatible(
|
||||
entry.is_some(),
|
||||
100,
|
||||
std::option::Option::Some(crate::SPL_TOKEN_SURFACE_CODE.to_string()),
|
||||
entry.map(|(_, code, _)| return code.to_string()),
|
||||
tag.map(|value| return format!("{value:02x}")),
|
||||
);
|
||||
}
|
||||
|
||||
fn decode(&self, input: &crate::CoreInstructionReplayInput) -> crate::DecoderExecutionResult {
|
||||
if input.program_id != kb_program_ids::SPL_TOKEN_PROGRAM_ID {
|
||||
return crate::DecoderExecutionResult::unsupported(std::option::Option::None);
|
||||
}
|
||||
let result = crate::spl_token_decode(input);
|
||||
if matches!(
|
||||
result.status,
|
||||
crate::DecoderOutcomeStatus::Failed | crate::DecoderOutcomeStatus::Unsupported
|
||||
) {
|
||||
tracing::error!(
|
||||
target: crate::SPL_TOKEN_TRACING_TARGET,
|
||||
action = "decode_failure",
|
||||
signature = %input.signature,
|
||||
slot = input.slot,
|
||||
instruction_path = %input.instruction_path,
|
||||
program_id = %input.program_id,
|
||||
processor_name = "spl_token",
|
||||
processor_version = env!("CARGO_PKG_VERSION"),
|
||||
input_key = %input.replay_input_key,
|
||||
payload_hash = ?input.instruction_payload_hash,
|
||||
result_status = ?result.status,
|
||||
diagnostics = ?result.diagnostics,
|
||||
"classic SPL Token instruction was not decoded successfully"
|
||||
);
|
||||
}
|
||||
tracing::debug!(
|
||||
target: crate::SPL_TOKEN_TRACING_TARGET,
|
||||
action = "decode",
|
||||
signature = %input.signature,
|
||||
instruction_path = %input.instruction_path,
|
||||
transaction_failed = input.transaction_failed,
|
||||
result_status = ?result.status,
|
||||
observation_count = result.observations.len(),
|
||||
"classic SPL Token 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!("Token replay input failed: {error}"),
|
||||
};
|
||||
}
|
||||
|
||||
fn minimal_wire(tag: u8) -> std::vec::Vec<u8> {
|
||||
return match tag {
|
||||
0 | 20 => {
|
||||
let mut value = std::vec![tag, 9];
|
||||
value.extend_from_slice(&[1; 32]);
|
||||
value.push(0);
|
||||
value
|
||||
},
|
||||
2 | 19 => std::vec![tag, 1],
|
||||
3 | 4 | 7 | 8 | 23 => {
|
||||
let mut value = std::vec![tag];
|
||||
value.extend_from_slice(&1_u64.to_le_bytes());
|
||||
value
|
||||
},
|
||||
6 => std::vec![tag, 0, 0],
|
||||
12 | 13 | 14 | 15 => {
|
||||
let mut value = std::vec![tag];
|
||||
value.extend_from_slice(&1_u64.to_le_bytes());
|
||||
value.push(9);
|
||||
value
|
||||
},
|
||||
16 | 18 => {
|
||||
let mut value = std::vec![tag];
|
||||
value.extend_from_slice(&[2; 32]);
|
||||
value
|
||||
},
|
||||
24 => std::vec![tag, b'1'],
|
||||
45 => std::vec![tag, 0],
|
||||
255 => std::vec![tag, 0, 1, 1],
|
||||
_ => std::vec![tag],
|
||||
};
|
||||
}
|
||||
|
||||
fn account(position: usize, signer: bool, writable: bool) -> serde_json::Value {
|
||||
return serde_json::json!({
|
||||
"position": position,
|
||||
"accountIndex": position,
|
||||
"accountKey": format!("account{position}"),
|
||||
"signer": signer,
|
||||
"writable": writable,
|
||||
"source": "static",
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_support_program_id_and_interface_match() {
|
||||
assert_eq!(spl_token_interface::ID.to_string(), kb_program_ids::SPL_TOKEN_PROGRAM_ID);
|
||||
let decoder = crate::SplTokenDecoder;
|
||||
assert_eq!(crate::InstructionDecoder::surfaces(&decoder).len(), 1);
|
||||
let observation = crate::ProgramObservation {
|
||||
signature: crate::Signature("signature".to_string()),
|
||||
slot: crate::Slot(1),
|
||||
instruction_path: crate::InstructionPath("0".to_string()),
|
||||
program_id: crate::ProgramId(kb_program_ids::SPL_TOKEN_PROGRAM_ID.to_string()),
|
||||
discriminator_8: std::option::Option::None,
|
||||
data_len: 1,
|
||||
accounts_len: 0,
|
||||
failed: false,
|
||||
};
|
||||
assert_eq!(
|
||||
crate::ProtocolDecoder::supports_observation(&decoder, &observation),
|
||||
crate::DecoderSupport::Yes
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coverage_matrix_and_compiled_entries_are_equal() {
|
||||
let matrix: serde_json::Value =
|
||||
match serde_json::from_str(include_str!("../../../../../docs/SPL_TOKEN_MATRIX.json")) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("SPL Token matrix is invalid: {error}"),
|
||||
};
|
||||
let rows = match matrix.get("instructions").and_then(serde_json::Value::as_array) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => panic!("SPL Token matrix has no instructions"),
|
||||
};
|
||||
let matrix_entries = rows
|
||||
.iter()
|
||||
.map(|row| {
|
||||
return (
|
||||
row.get("tag").and_then(serde_json::Value::as_u64).unwrap_or(u64::MAX) as u8,
|
||||
row.get("name").and_then(serde_json::Value::as_str).unwrap_or(""),
|
||||
);
|
||||
})
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
let compiled_entries = crate::SPL_TOKEN_INSTRUCTION_ENTRIES
|
||||
.iter()
|
||||
.map(|(tag, code, _)| return (*tag, *code))
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
assert_eq!(matrix_entries, compiled_entries);
|
||||
assert_eq!(crate::InstructionDecoder::coverage(&crate::SplTokenDecoder).len(), 28);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cluster_evidence_scopes_recent_instruction_deployment_exactly() {
|
||||
let matrix: serde_json::Value =
|
||||
match serde_json::from_str(include_str!("../../../../../docs/SPL_TOKEN_MATRIX.json")) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("SPL Token matrix is invalid: {error}"),
|
||||
};
|
||||
assert_eq!(
|
||||
matrix["deploymentAudit"]["mainnet"]["status"],
|
||||
"real_corpus_decode_and_materialization_validated"
|
||||
);
|
||||
assert_eq!(matrix["corpusAudit"]["mainnet"]["invariants"]["uncommittedOutputs"], 0);
|
||||
assert_eq!(matrix["corpusAudit"]["mainnet"]["invariants"]["secondReplaySelected"], 0);
|
||||
assert_eq!(matrix["corpusAudit"]["devnet"]["recentInstructionProbe"]["submission"], false);
|
||||
assert_eq!(
|
||||
matrix["corpusAudit"]["devnet"]["recentInstructionProbe"]["batch"]["success"],
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
matrix["corpusAudit"]["devnet"]["recentInstructionProbe"]["batch"]["computeUnits"],
|
||||
270
|
||||
);
|
||||
assert_eq!(
|
||||
matrix["corpusAudit"]["devnet"]["recentInstructionProbe"]["unwrapLamports"]["success"],
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
matrix["corpusAudit"]["devnet"]["recentInstructionProbe"]["unwrapLamports"]["computeUnits"],
|
||||
140
|
||||
);
|
||||
|
||||
let instructions = match matrix.get("instructions").and_then(serde_json::Value::as_array) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => panic!("SPL Token matrix has no instructions"),
|
||||
};
|
||||
for recent_name in ["unwrap_lamports", "batch"] {
|
||||
let recent = match instructions.iter().find(|row| {
|
||||
return row.get("name").and_then(serde_json::Value::as_str)
|
||||
== std::option::Option::Some(recent_name);
|
||||
}) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
panic!("recent SPL Token instruction {recent_name} is absent")
|
||||
},
|
||||
};
|
||||
assert_eq!(recent["executorSupport"]["constructibility"], "official_builder_proven");
|
||||
assert_eq!(
|
||||
recent["executorSupport"]["clusterDeployment"],
|
||||
"devnet_simulation_succeeded_2026-07-16"
|
||||
);
|
||||
assert_eq!(recent["clusterStatus"]["localnet"], "not_tested");
|
||||
assert_eq!(recent["clusterStatus"]["devnet"], "simulation_succeeded_2026-07-16");
|
||||
assert_eq!(recent["clusterStatus"]["mainnet"], "not_tested");
|
||||
assert_eq!(recent["realSignatures"].as_array().map(std::vec::Vec::len), Some(0));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_published_tag_decodes_and_matches_official_unpack() {
|
||||
for (tag, code, _) in crate::SPL_TOKEN_INSTRUCTION_ENTRIES {
|
||||
let wire = minimal_wire(*tag);
|
||||
let official = spl_token_interface::instruction::TokenInstruction::unpack(&wire);
|
||||
assert!(official.is_ok(), "official unpack rejected tag {tag}");
|
||||
let input = input(
|
||||
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
wire.as_slice(),
|
||||
serde_json::json!([]),
|
||||
serde_json::json!([]),
|
||||
false,
|
||||
"0",
|
||||
);
|
||||
let result = crate::InstructionDecoder::decode(&crate::SplTokenDecoder, &input);
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
assert_eq!(result.recognized_entry_code.as_deref(), std::option::Option::Some(*code));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_transfer_is_an_exact_uncommitted_inner_intent_without_invented_mint() {
|
||||
let mut wire = std::vec![3];
|
||||
wire.extend_from_slice(&u64::MAX.to_le_bytes());
|
||||
let accounts = serde_json::json!([
|
||||
{"position":0,"accountIndex":0,"accountKey":"source"},
|
||||
{"position":1,"accountIndex":1,"accountKey":"destination"},
|
||||
{"position":2,"accountIndex":2,"accountKey":"authority"}
|
||||
]);
|
||||
let keys = serde_json::json!([
|
||||
{"accountIndex":0,"accountKey":"source","signer":false,"writable":true,"source":"static"},
|
||||
{"accountIndex":1,"accountKey":"destination","signer":false,"writable":true,"source":"static"},
|
||||
{"accountIndex":2,"accountKey":"authority","signer":true,"writable":false,"source":"static"}
|
||||
]);
|
||||
let input = input(
|
||||
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
wire.as_slice(),
|
||||
accounts,
|
||||
keys,
|
||||
true,
|
||||
"2/1",
|
||||
);
|
||||
let result = crate::InstructionDecoder::decode(&crate::SplTokenDecoder, &input);
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
assert!(!result.observations[0].observation_committed);
|
||||
assert_eq!(
|
||||
result.observations[0].payload_json["parameters"]["amountRaw"],
|
||||
u64::MAX.to_string()
|
||||
);
|
||||
assert_eq!(result.observations[0].payload_json["inference"]["mintInvented"], false);
|
||||
assert_eq!(
|
||||
result.observations[0].event.source_kind,
|
||||
crate::EventSourceKind::InnerInstruction
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn suffix_unknown_empty_truncated_and_invalid_utf8_are_bounded() {
|
||||
let mut transfer = minimal_wire(3);
|
||||
transfer.extend_from_slice(&[0xaa, 0xbb]);
|
||||
let suffix = input(
|
||||
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
transfer.as_slice(),
|
||||
serde_json::json!([]),
|
||||
serde_json::json!([]),
|
||||
false,
|
||||
"0",
|
||||
);
|
||||
let suffix_result = crate::InstructionDecoder::decode(&crate::SplTokenDecoder, &suffix);
|
||||
assert_eq!(suffix_result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
assert_eq!(suffix_result.observations[0].payload_json["wire"]["suffixLengthBytes"], 2);
|
||||
for wire in [std::vec::Vec::new(), std::vec![3, 1], std::vec![24, 0xff], std::vec![255]] {
|
||||
let input = input(
|
||||
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
wire.as_slice(),
|
||||
serde_json::json!([]),
|
||||
serde_json::json!([]),
|
||||
false,
|
||||
"0",
|
||||
);
|
||||
let result = crate::InstructionDecoder::decode(&crate::SplTokenDecoder, &input);
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Failed);
|
||||
assert!(result.observations.is_empty());
|
||||
}
|
||||
let unknown = input(
|
||||
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
&[44],
|
||||
serde_json::json!([]),
|
||||
serde_json::json!([]),
|
||||
false,
|
||||
"0",
|
||||
);
|
||||
let result = crate::InstructionDecoder::decode(&crate::SplTokenDecoder, &unknown);
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Unsupported);
|
||||
assert!(!result.diagnostics.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_preserves_order_slices_paths_and_rejects_nested_batch() {
|
||||
let mut batch = std::vec![255, 0, 9, 3];
|
||||
batch.extend_from_slice(&1_u64.to_le_bytes());
|
||||
batch.extend_from_slice(&[0, 1, 17]);
|
||||
let cinput = input(
|
||||
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
batch.as_slice(),
|
||||
serde_json::json!([]),
|
||||
serde_json::json!([]),
|
||||
false,
|
||||
"4/2",
|
||||
);
|
||||
let result = crate::InstructionDecoder::decode(&crate::SplTokenDecoder, &cinput);
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
assert_eq!(result.observations.len(), 3);
|
||||
assert_eq!(result.observations[1].event.instruction_path.0, "4/2/batch/0");
|
||||
assert_eq!(result.observations[2].event.instruction_path.0, "4/2/batch/1");
|
||||
assert_eq!(result.observations[0].payload_json["parameters"]["subInstructionCount"], 2);
|
||||
let nested = input(
|
||||
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
&[255, 0, 1, 255],
|
||||
serde_json::json!([]),
|
||||
serde_json::json!([]),
|
||||
false,
|
||||
"0",
|
||||
);
|
||||
let nested_result = crate::InstructionDecoder::decode(&crate::SplTokenDecoder, &nested);
|
||||
assert_eq!(nested_result.status, crate::DecoderOutcomeStatus::Failed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authority_forms_and_multisig_threshold_diagnostics_are_explicit() {
|
||||
let wire = minimal_wire(3);
|
||||
let simple_accounts = serde_json::json!([
|
||||
{"position":0,"accountIndex":0,"accountKey":"account0"},
|
||||
{"position":1,"accountIndex":1,"accountKey":"account1"},
|
||||
{"position":2,"accountIndex":2,"accountKey":"account2"}
|
||||
]);
|
||||
let simple_keys = serde_json::json!([
|
||||
account(0, false, true),
|
||||
account(1, false, true),
|
||||
account(2, true, false)
|
||||
]);
|
||||
let simple = input(
|
||||
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
wire.as_slice(),
|
||||
simple_accounts.clone(),
|
||||
simple_keys,
|
||||
false,
|
||||
"0",
|
||||
);
|
||||
let simple_result = crate::InstructionDecoder::decode(&crate::SplTokenDecoder, &simple);
|
||||
assert_eq!(simple_result.observations[0].payload_json["authority"]["form"], "single");
|
||||
let mut multisig_accounts = simple_accounts.as_array().cloned().unwrap_or_default();
|
||||
multisig_accounts
|
||||
.push(serde_json::json!({"position":3,"accountIndex":3,"accountKey":"account3"}));
|
||||
let multisig_keys = serde_json::json!([
|
||||
account(0, false, true),
|
||||
account(1, false, true),
|
||||
account(2, false, false),
|
||||
account(3, true, false)
|
||||
]);
|
||||
let multisig = input(
|
||||
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
wire.as_slice(),
|
||||
serde_json::Value::Array(multisig_accounts),
|
||||
multisig_keys,
|
||||
false,
|
||||
"0",
|
||||
);
|
||||
let multisig_result = crate::InstructionDecoder::decode(&crate::SplTokenDecoder, &multisig);
|
||||
assert_eq!(multisig_result.observations[0].payload_json["authority"]["form"], "multisig");
|
||||
assert_eq!(
|
||||
multisig_result.observations[0].payload_json["authority"]["statefulMultisigValidation"],
|
||||
"requires_multisig_account_snapshot"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user