This commit is contained in:
2026-07-23 16:37:12 +02:00
parent 99c345f2f2
commit 0da75c1311
2159 changed files with 230833 additions and 0 deletions

View File

@@ -0,0 +1,576 @@
// 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<std::string::String>,
version: impl std::convert::Into<std::string::String>,
) -> kb_core::Result<Self> {
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<std::string::String>,
/// 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<std::string::String>,
/// 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<std::string::String>,
surface_code: std::option::Option<std::string::String>,
entry_kind: DecoderCoverageEntryKind,
entry_code: impl std::convert::Into<std::string::String>,
discriminator_hex: std::option::Option<std::string::String>,
historical: bool,
) -> kb_core::Result<Self> {
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<std::string::String>,
/// Optional recognized instruction or discriminator code.
pub entry_code: std::option::Option<std::string::String>,
/// Optional normalized lowercase hexadecimal discriminator.
pub discriminator_hex: std::option::Option<std::string::String>,
}
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<std::string::String>,
entry_code: std::option::Option<std::string::String>,
discriminator_hex: std::option::Option<std::string::String>,
) -> 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<std::string::String>,
}
/// 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<std::string::String>,
message: impl std::convert::Into<std::string::String>,
retriable: bool,
) -> kb_core::Result<Self> {
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<serde_json::Value>,
/// 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<std::string::String>,
/// Decoded observations.
pub observations: std::vec::Vec<DecodedObservation>,
/// Structured diagnostics.
pub diagnostics: std::vec::Vec<DecoderDiagnostic>,
}
impl DecoderExecutionResult {
/// Builds an unsupported result with no false decoded observation.
pub fn unsupported(entry_code: std::option::Option<std::string::String>) -> 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<std::string::String>) -> 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<DecoderCoverageDeclaration>;
/// 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<std::string::String> {
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<std::string::String> {
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<std::string::String> {
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::<std::vec::Vec<_>>();
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());
}
}