// file: kb-lib/src/decoder/api/contracts.rs // version: 7 //! Backend-neutral contextual instruction decoder contracts. use sha2::Digest; // rust-rules: trait-import /// Current contextual core instruction input contract version. pub const CORE_INSTRUCTION_INPUT_CONTRACT_VERSION: u32 = crate::CORE_REPLAY_INPUT_CONTRACT_VERSION; /// Stable identity of one contextual instruction decoder. #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] pub struct DecoderIdentity { /// Stable lower snake case processor name. pub name: std::string::String, /// Semantic or deterministic implementation version. pub version: std::string::String, } impl DecoderIdentity { /// Builds a validated decoder identity. pub fn new( name: impl std::convert::Into, version: impl std::convert::Into, ) -> kb_core::Result { let value = Self { name: name.into(), version: version.into(), }; if value.name.trim().is_empty() || value.version.trim().is_empty() { return std::result::Result::Err(kb_core::Error::invalid_state( "decoder identity fields must not be empty", )); } return std::result::Result::Ok(value); } } /// One statically declared Solana surface supported by a decoder. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct DecoderSurface { /// Exact Solana program identifier. pub program_id: &'static str, /// Stable surface code. pub surface_code: &'static str, /// Explicit dispatch priority, where larger values win. pub priority: u16, } /// Declared decoder coverage entry kind. #[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] pub enum DecoderCoverageEntryKind { /// Program instruction entry. Instruction, /// Program event entry. Event, /// Raw discriminator entry. Discriminator, } /// Machine-readable decoder coverage declaration. #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] pub struct DecoderCoverageDeclaration { /// Solana program identifier. pub program_id: std::string::String, /// Optional stable surface code. pub surface_code: std::option::Option, /// Entry kind. pub entry_kind: DecoderCoverageEntryKind, /// Stable instruction, event or discriminator code. pub entry_code: std::string::String, /// Optional normalized lowercase hexadecimal discriminator. pub discriminator_hex: std::option::Option, /// Whether the entry is historical or deprecated but still supported. pub historical: bool, } impl DecoderCoverageDeclaration { /// Builds a validated coverage declaration. pub fn new( program_id: impl std::convert::Into, surface_code: std::option::Option, entry_kind: DecoderCoverageEntryKind, entry_code: impl std::convert::Into, discriminator_hex: std::option::Option, historical: bool, ) -> kb_core::Result { let value = Self { program_id: program_id.into(), surface_code, entry_kind, entry_code: entry_code.into(), discriminator_hex, historical, }; if value.program_id.trim().is_empty() || value.entry_code.trim().is_empty() { return std::result::Result::Err(kb_core::Error::invalid_state( "decoder coverage program id and entry code must not be empty", )); } if value .surface_code .as_deref() .is_some_and(|surface| return surface.trim().is_empty()) { return std::result::Result::Err(kb_core::Error::invalid_state( "decoder coverage surface code must not be empty when present", )); } if value .discriminator_hex .as_deref() .is_some_and(|discriminator| return discriminator.trim().is_empty()) { return std::result::Result::Err(kb_core::Error::invalid_state( "decoder coverage discriminator must not be empty when present", )); } return std::result::Result::Ok(value); } } /// Deterministic recognition result used by dispatch. #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] pub struct DecoderRecognition { /// Whether the decoder is compatible with the input. pub compatible: bool, /// Whether recognition is exact rather than program-only. pub exact: bool, /// Explicit priority used after program and surface matching. pub priority: u16, /// Optional recognized surface code. pub surface_code: std::option::Option, /// Optional recognized instruction or discriminator code. pub entry_code: std::option::Option, /// Optional normalized lowercase hexadecimal discriminator. pub discriminator_hex: std::option::Option, } impl DecoderRecognition { /// Builds a non-compatible recognition result. pub fn incompatible() -> Self { return Self { compatible: false, exact: false, priority: 0, surface_code: std::option::Option::None, entry_code: std::option::Option::None, discriminator_hex: std::option::Option::None, }; } /// Builds a compatible recognition result. pub fn compatible( exact: bool, priority: u16, surface_code: std::option::Option, entry_code: std::option::Option, discriminator_hex: std::option::Option, ) -> Self { return Self { compatible: true, exact, priority, surface_code, entry_code, discriminator_hex, }; } } /// Decoder terminal result status. #[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] pub enum DecoderOutcomeStatus { /// At least one stable decoded observation was produced. Decoded, /// The decoder intentionally ignored a recognized input. Ignored, /// The input belongs to the program but its entry is unsupported. Unsupported, /// Decoding failed and diagnostics were produced. Failed, } /// Proof category attached to one decoded observation. #[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] pub enum DecoderProofKind { /// Exact instruction discriminator match. ExactDiscriminator, /// Exact binary layout decode. ExactLayout, /// Exact IDL-based decode. Idl, /// Manually verified exact decode. Manual, /// Correlation with runtime logs. LogCorrelation, /// Balance delta evidence. BalanceDelta, /// Heuristic evidence that must not be treated as exact. Heuristic, /// Audit-only classification. Audit, /// Unknown or unclassified evidence. Unknown, } /// Explicit proof attached to one decoded observation. #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] pub struct DecoderProof { /// Proof category. pub kind: DecoderProofKind, /// Decoder confidence. pub confidence: crate::DecoderConfidence, /// Stable evidence strings, hashes or classifier codes. pub evidence: std::vec::Vec, } /// Structured decoder diagnostic without third-party error wrappers. #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] pub struct DecoderDiagnostic { /// Stable machine-readable diagnostic code. pub code: std::string::String, /// Human-readable diagnostic message. pub message: std::string::String, /// Whether retrying the same version and input may succeed. pub retriable: bool, } impl DecoderDiagnostic { /// Builds a validated decoder diagnostic. pub fn new( code: impl std::convert::Into, message: impl std::convert::Into, retriable: bool, ) -> kb_core::Result { let value = Self { code: code.into(), message: message.into(), retriable, }; if value.code.trim().is_empty() || value.message.trim().is_empty() { return std::result::Result::Err(kb_core::Error::invalid_state( "decoder diagnostic code and message must not be empty", )); } return std::result::Result::Ok(value); } } /// Stable contextual decoded observation persisted by the decode pipeline. #[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)] pub struct DecodedObservation { /// Stable processor-owned event key within one input. pub event_key: std::string::String, /// Shared typed protocol event identity. pub event: crate::DecodedProtocolEvent, /// Typed event payload serialized as deterministic JSON. pub payload_json: serde_json::Value, /// Whether the source transaction failed on-chain. pub transaction_failed: bool, /// Optional transaction error JSON. pub transaction_error: std::option::Option, /// Whether the observed state mutation was committed on-chain. pub observation_committed: bool, /// Explicit decoding proof. pub proof: DecoderProof, } impl DecodedObservation { /// Validates failed transaction commit semantics and stable identity fields. pub fn validate(&self) -> kb_core::Result<()> { if self.event_key.trim().is_empty() { return std::result::Result::Err(kb_core::Error::invalid_state( "decoded observation event key must not be empty", )); } if self.transaction_failed && self.observation_committed { return std::result::Result::Err(kb_core::Error::invalid_state( "failed transaction observations must not be marked committed", )); } if self.event.signature.0.trim().is_empty() || self.event.instruction_path.0.trim().is_empty() || self.event.program_id.0.trim().is_empty() || self.event.event_code.0.trim().is_empty() { return std::result::Result::Err(kb_core::Error::invalid_state( "decoded observation event identity fields must not be empty", )); } return std::result::Result::Ok(()); } } /// Complete explicit result produced by one decoder invocation. #[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)] pub struct DecoderExecutionResult { /// Terminal decoder status. pub status: DecoderOutcomeStatus, /// Optional recognized instruction or discriminator code. pub recognized_entry_code: std::option::Option, /// Decoded observations. pub observations: std::vec::Vec, /// Structured diagnostics. pub diagnostics: std::vec::Vec, } impl DecoderExecutionResult { /// Builds an unsupported result with no false decoded observation. pub fn unsupported(entry_code: std::option::Option) -> Self { return Self { status: DecoderOutcomeStatus::Unsupported, recognized_entry_code: entry_code, observations: std::vec::Vec::new(), diagnostics: std::vec::Vec::new(), }; } /// Builds an ignored result. pub fn ignored(entry_code: std::option::Option) -> Self { return Self { status: DecoderOutcomeStatus::Ignored, recognized_entry_code: entry_code, observations: std::vec::Vec::new(), diagnostics: std::vec::Vec::new(), }; } /// Validates status and observation consistency. pub fn validate(&self) -> kb_core::Result<()> { if self.status == DecoderOutcomeStatus::Decoded && self.observations.is_empty() { return std::result::Result::Err(kb_core::Error::invalid_state( "decoded status requires at least one observation", )); } if self.status != DecoderOutcomeStatus::Decoded && !self.observations.is_empty() { return std::result::Result::Err(kb_core::Error::invalid_state( "non-decoded status must not contain observations", )); } if self.status == DecoderOutcomeStatus::Failed && self.diagnostics.is_empty() { return std::result::Result::Err(kb_core::Error::invalid_state( "failed decoder status requires at least one diagnostic", )); } for observation in &self.observations { let validation_result = observation.validate(); if let std::result::Result::Err(error) = validation_result { return std::result::Result::Err(error); } } return std::result::Result::Ok(()); } } /// Contextual instruction decoder contract used by the common pipeline. pub trait InstructionDecoder: std::marker::Send + std::marker::Sync { /// Returns the stable decoder identity. fn identity(&self) -> DecoderIdentity; /// Returns every exact Solana surface supported by the decoder. fn surfaces(&self) -> &'static [DecoderSurface]; /// Returns the machine-readable declared coverage matrix. fn coverage(&self) -> std::vec::Vec; /// Recognizes one contextual core instruction for deterministic dispatch. fn recognize(&self, input: &crate::CoreInstructionReplayInput) -> DecoderRecognition; /// Decodes one recognized contextual core instruction. fn decode(&self, input: &crate::CoreInstructionReplayInput) -> DecoderExecutionResult; } /// Returns true when a decoder declares the exact program id. pub fn decoder_handles_program_id(decoder: &dyn InstructionDecoder, program_id: &str) -> bool { for surface in decoder.surfaces() { if surface.program_id == program_id { return true; } } return false; } /// Computes a deterministic lowercase SHA-256 hash for one contextual input. pub fn contextual_input_hash( input: &crate::CoreInstructionReplayInput, ) -> kb_core::Result { let serialization_result = serde_json::to_value(input); let mut value = match serialization_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return std::result::Result::Err(kb_core::Error::json(format!( "cannot serialize contextual decoder input: {error}" ))); }, }; return crate::deterministic_json_hash(&mut value); } /// Computes a deterministic lowercase SHA-256 hash for one JSON value. pub fn deterministic_json_hash( value: &mut serde_json::Value, ) -> kb_core::Result { sort_json_value(value); let bytes_result = serde_json::to_vec(value); let bytes = match bytes_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return std::result::Result::Err(kb_core::Error::json(format!( "cannot serialize deterministic JSON bytes: {error}" ))); }, }; 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 std::result::Result::Ok(output); } /// Extracts the first eight payload bytes as normalized lowercase hexadecimal when available. pub fn discriminator_8_hex( input: &crate::CoreInstructionReplayInput, ) -> std::option::Option { let payload = match &input.instruction_payload_json { std::option::Option::Some(value) => value, std::option::Option::None => return std::option::Option::None, }; 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::option::Option::None, }; let decoded_result = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, encoded.as_bytes()); let decoded = match decoded_result { std::result::Result::Ok(value) => value, std::result::Result::Err(_error) => return std::option::Option::None, }; if decoded.len() < 8 { return std::option::Option::None; } let mut output = std::string::String::with_capacity(16); for byte in &decoded[..8] { output.push_str(format!("{byte:02x}").as_str()); } return std::option::Option::Some(output); } fn sort_json_value(value: &mut serde_json::Value) { match value { serde_json::Value::Object(map) => { let mut entries = std::mem::take(map).into_iter().collect::>(); entries.sort_by(|left, right| return left.0.cmp(&right.0)); for (key, mut child) in entries { sort_json_value(&mut child); map.insert(key, child); } }, serde_json::Value::Array(values) => { for child in values { sort_json_value(child); } }, _ => (), } } #[cfg(test)] mod tests { fn replay_input() -> crate::CoreInstructionReplayInput { let result = crate::CoreInstructionReplayInput::new( "signature:0", "signature", 42, "0", "program", false, std::option::Option::None, serde_json::json!([{"accountIndex": 0, "accountKey": "account"}]), serde_json::json!([{"accountIndex": 0, "accountKey": "account"}]), std::option::Option::Some(serde_json::json!({"dataBase64": "AQIDBAUGBwg="})), std::option::Option::Some("payload-hash".to_string()), serde_json::json!([{ "instructionIndex": 0, "instructionPath": "0", "programId": "program", "payloadJson": {"dataBase64": "AQIDBAUGBwg="}, "payloadHash": "payload-hash" }]), serde_json::json!([]), serde_json::json!([]), serde_json::json!([]), ); return match result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("unexpected replay input error: {error}"), }; } #[test] fn contextual_input_hash_is_stable() { let first = crate::contextual_input_hash(&replay_input()); let second = crate::contextual_input_hash(&replay_input()); assert!(first.is_ok()); assert_eq!(first, second); } #[test] fn contextual_input_hash_changes_when_outer_context_changes() { let first = replay_input(); let mut second = replay_input(); second.outer_instructions_json = serde_json::json!([{ "instructionIndex": 0, "instructionPath": "0", "programId": "program", "payloadJson": {"dataBase64": "CAcGBQQDAgE="}, "payloadHash": "different-payload-hash" }]); let first_hash = crate::contextual_input_hash(&first); let second_hash = crate::contextual_input_hash(&second); assert!(first_hash.is_ok()); assert!(second_hash.is_ok()); assert_ne!(first_hash, second_hash); } #[test] fn contextual_input_serialization_is_deterministic_with_outer_context() { let first_result = serde_json::to_string(&replay_input()); let first = match first_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("unexpected serialization error: {error}"), }; let second_result = serde_json::to_string(&replay_input()); let second = match second_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("unexpected serialization error: {error}"), }; assert_eq!(first, second); } #[test] fn discriminator_uses_first_eight_bytes() { let value = crate::discriminator_8_hex(&replay_input()); assert_eq!(value.as_deref(), std::option::Option::Some("0102030405060708")); } #[test] fn failed_observation_cannot_be_committed() { let observation = crate::DecodedObservation { event_key: "event".to_string(), event: crate::DecodedProtocolEvent { signature: crate::Signature("signature".to_string()), slot: crate::Slot(42), instruction_path: crate::InstructionPath("0".to_string()), program_id: crate::ProgramId("program".to_string()), protocol_code: crate::ProtocolCode("solana".to_string()), surface_code: crate::SurfaceCode("system_program".to_string()), event_code: crate::EventCode("system_program.attempt".to_string()), event_name: crate::EventName("attempt".to_string()), event_family: crate::EventFamily::Audit, source_kind: crate::EventSourceKind::Instruction, confidence: crate::DecoderConfidence::Exact, }, payload_json: serde_json::json!({}), transaction_failed: true, transaction_error: std::option::Option::Some( serde_json::json!({"InstructionError": [0, "Custom"]}), ), observation_committed: true, proof: crate::DecoderProof { kind: crate::DecoderProofKind::ExactLayout, confidence: crate::DecoderConfidence::Exact, evidence: std::vec!["layout".to_string()], }, }; assert!(observation.validate().is_err()); } }