514 lines
20 KiB
Rust
514 lines
20 KiB
Rust
// file: kb-lib/src/decoder/spl/token/decoder.rs
|
|
// version: 4
|
|
|
|
//! Exact classic SPL Token dispatch for the common decode pipeline.
|
|
|
|
const TOKEN_SURFACES: &[crate::DcApiDecoderSurface] = &[crate::DcApiDecoderSurface {
|
|
program_id: kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
|
surface_code: crate::DC_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 DcSplTokenDecoder;
|
|
|
|
impl crate::DcApiProtocolDecoder for crate::DcSplTokenDecoder {
|
|
fn decoder_name(&self) -> &'static str {
|
|
return "kb-lib.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::MdProgramObservation,
|
|
) -> crate::DcApiDecoderSupport {
|
|
return if crate::DcApiProtocolDecoder::handles_program_id(self, &observation.program_id) {
|
|
crate::DcApiDecoderSupport::Yes
|
|
} else {
|
|
crate::DcApiDecoderSupport::No
|
|
};
|
|
}
|
|
|
|
fn decode_observation(
|
|
&self,
|
|
_observation: &crate::MdProgramObservation,
|
|
) -> kb_core::Result<std::vec::Vec<crate::MdDecodedProtocolEvent>> {
|
|
return std::result::Result::Ok(std::vec::Vec::new());
|
|
}
|
|
}
|
|
|
|
impl crate::DcApiInstructionDecoder for crate::DcSplTokenDecoder {
|
|
fn identity(&self) -> crate::DcApiDecoderIdentity {
|
|
return crate::DcApiDecoderIdentity {
|
|
name: "spl.token".to_string(),
|
|
version: env!("CARGO_PKG_VERSION").to_string(),
|
|
};
|
|
}
|
|
|
|
fn surfaces(&self) -> &'static [crate::DcApiDecoderSurface] {
|
|
return TOKEN_SURFACES;
|
|
}
|
|
|
|
fn coverage(&self) -> std::vec::Vec<crate::DcApiDecoderCoverageDeclaration> {
|
|
return crate::DC_SPL_TOKEN_INSTRUCTION_ENTRIES
|
|
.iter()
|
|
.map(|(tag, code, historical)| {
|
|
return crate::DcApiDecoderCoverageDeclaration {
|
|
program_id: kb_program_ids::SPL_TOKEN_PROGRAM_ID.to_string(),
|
|
surface_code: std::option::Option::Some(
|
|
crate::DC_SPL_TOKEN_SURFACE_CODE.to_string(),
|
|
),
|
|
entry_kind: crate::DcApiDecoderCoverageEntryKind::Instruction,
|
|
entry_code: (*code).to_string(),
|
|
discriminator_hex: std::option::Option::Some(format!("{tag:02x}")),
|
|
historical: *historical,
|
|
};
|
|
})
|
|
.collect();
|
|
}
|
|
|
|
fn recognize(
|
|
&self,
|
|
input: &crate::MdCoreInstructionReplayInput,
|
|
) -> crate::DcApiDecoderRecognition {
|
|
if input.program_id != kb_program_ids::SPL_TOKEN_PROGRAM_ID {
|
|
return crate::DcApiDecoderRecognition::incompatible();
|
|
}
|
|
let tag = crate::decoder_spl_token_payload_tag(input);
|
|
let entry = tag.and_then(crate::decoder_spl_token_entry_for_tag);
|
|
return crate::DcApiDecoderRecognition::compatible(
|
|
entry.is_some(),
|
|
100,
|
|
std::option::Option::Some(crate::DC_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::MdCoreInstructionReplayInput,
|
|
) -> crate::DcApiDecoderExecutionResult {
|
|
if input.program_id != kb_program_ids::SPL_TOKEN_PROGRAM_ID {
|
|
return crate::DcApiDecoderExecutionResult::unsupported(std::option::Option::None);
|
|
}
|
|
let result = crate::decoder_spl_token_decode(input);
|
|
if matches!(
|
|
result.status,
|
|
crate::DcApiDecoderOutcomeStatus::Failed
|
|
| crate::DcApiDecoderOutcomeStatus::Unsupported
|
|
) {
|
|
tracing::error!(
|
|
target: crate::TRACING_TARGET_DECODER_SPL_TOKEN,
|
|
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::TRACING_TARGET_DECODER_SPL_TOKEN,
|
|
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::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!("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::DcSplTokenDecoder;
|
|
assert_eq!(crate::DcApiInstructionDecoder::surfaces(&decoder).len(), 1);
|
|
let observation = crate::MdProgramObservation {
|
|
signature: crate::MdSignature("signature".to_string()),
|
|
slot: crate::MdSlot(1),
|
|
instruction_path: crate::MdInstructionPath("0".to_string()),
|
|
program_id: crate::MdProgramId(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::DcApiProtocolDecoder::supports_observation(&decoder, &observation),
|
|
crate::DcApiDecoderSupport::Yes
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn coverage_matrix_and_compiled_entries_are_equal() {
|
|
let matrix: serde_json::Value = match serde_json::from_str(include_str!(
|
|
"../../../../../test-fixtures/contract-matrices/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::DC_SPL_TOKEN_INSTRUCTION_ENTRIES
|
|
.iter()
|
|
.map(|(tag, code, _)| return (*tag, *code))
|
|
.collect::<std::vec::Vec<_>>();
|
|
assert_eq!(matrix_entries, compiled_entries);
|
|
assert_eq!(crate::DcApiInstructionDecoder::coverage(&crate::DcSplTokenDecoder).len(), 28);
|
|
}
|
|
|
|
#[test]
|
|
fn cluster_evidence_scopes_recent_instruction_deployment_exactly() {
|
|
let matrix: serde_json::Value = match serde_json::from_str(include_str!(
|
|
"../../../../../test-fixtures/contract-matrices/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::DC_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::DcApiInstructionDecoder::decode(&crate::DcSplTokenDecoder, &input);
|
|
assert_eq!(result.status, crate::DcApiDecoderOutcomeStatus::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::DcApiInstructionDecoder::decode(&crate::DcSplTokenDecoder, &input);
|
|
assert_eq!(result.status, crate::DcApiDecoderOutcomeStatus::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::MdEventSourceKind::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::DcApiInstructionDecoder::decode(&crate::DcSplTokenDecoder, &suffix);
|
|
assert_eq!(suffix_result.status, crate::DcApiDecoderOutcomeStatus::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::DcApiInstructionDecoder::decode(&crate::DcSplTokenDecoder, &input);
|
|
assert_eq!(result.status, crate::DcApiDecoderOutcomeStatus::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::DcApiInstructionDecoder::decode(&crate::DcSplTokenDecoder, &unknown);
|
|
assert_eq!(result.status, crate::DcApiDecoderOutcomeStatus::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::DcApiInstructionDecoder::decode(&crate::DcSplTokenDecoder, &cinput);
|
|
assert_eq!(result.status, crate::DcApiDecoderOutcomeStatus::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::DcApiInstructionDecoder::decode(&crate::DcSplTokenDecoder, &nested);
|
|
assert_eq!(nested_result.status, crate::DcApiDecoderOutcomeStatus::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::DcApiInstructionDecoder::decode(&crate::DcSplTokenDecoder, &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::DcApiInstructionDecoder::decode(&crate::DcSplTokenDecoder, &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"
|
|
);
|
|
}
|
|
}
|