|
|
|
|
@@ -0,0 +1,577 @@
|
|
|
|
|
// file: kb_decoder_api/src/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 =
|
|
|
|
|
kb_store_core::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: kb_model::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: kb_model::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: &kb_store_core::CoreInstructionReplayInput) -> DecoderRecognition;
|
|
|
|
|
/// Decodes one recognized contextual core instruction.
|
|
|
|
|
fn decode(&self, input: &kb_store_core::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: &kb_store_core::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: &kb_store_core::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() -> kb_store_core::CoreInstructionReplayInput {
|
|
|
|
|
let result = kb_store_core::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: kb_model::DecodedProtocolEvent {
|
|
|
|
|
signature: kb_model::Signature("signature".to_string()),
|
|
|
|
|
slot: kb_model::Slot(42),
|
|
|
|
|
instruction_path: kb_model::InstructionPath("0".to_string()),
|
|
|
|
|
program_id: kb_model::ProgramId("program".to_string()),
|
|
|
|
|
protocol_code: kb_model::ProtocolCode("solana".to_string()),
|
|
|
|
|
surface_code: kb_model::SurfaceCode("system_program".to_string()),
|
|
|
|
|
event_code: kb_model::EventCode("system_program.attempt".to_string()),
|
|
|
|
|
event_name: kb_model::EventName("attempt".to_string()),
|
|
|
|
|
event_family: kb_model::EventFamily::Audit,
|
|
|
|
|
source_kind: kb_model::EventSourceKind::Instruction,
|
|
|
|
|
confidence: kb_model::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: kb_model::DecoderConfidence::Exact,
|
|
|
|
|
evidence: std::vec!["layout".to_string()],
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
assert!(observation.validate().is_err());
|
|
|
|
|
}
|
|
|
|
|
}
|