0.1.0-pre.004
This commit is contained in:
@@ -1,13 +1,10 @@
|
||||
// file: kb-lib/src/decoder/api.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
//! Decoder contracts consolidated from `kb_decoder_api`.
|
||||
|
||||
/// Decoder API constants.
|
||||
pub mod constants;
|
||||
pub mod contracts;
|
||||
pub mod decoder;
|
||||
/// Canonical tracing target for this crate.
|
||||
/// Current contextual core instruction input contract version.
|
||||
pub use crate::decoder::api::contracts::CORE_INSTRUCTION_INPUT_CONTRACT_VERSION;
|
||||
/// Stable contextual decoded observation.
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
// file: kb-lib/src/decoder/api/constants.rs
|
||||
// version: 1
|
||||
|
||||
/// Canonical tracing target for this crate.
|
||||
pub(crate) const TRACING_TARGET: &str = "kb_decoder_api";
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-lib/src/decoder/api/decoder.rs
|
||||
// version: 4
|
||||
// version: 5
|
||||
|
||||
//! Shared decoder contracts for protocol decoder crates.
|
||||
|
||||
@@ -68,7 +68,6 @@ impl ProtocolDecoder for InitialDecoder {
|
||||
&self,
|
||||
_observation: &crate::ProgramObservation,
|
||||
) -> kb_core::Result<std::vec::Vec<crate::DecodedProtocolEvent>> {
|
||||
let _target = crate::TRACING_TARGET;
|
||||
return std::result::Result::Ok(std::vec::Vec::new());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,95 @@
|
||||
// file: kb-lib/src/decoder/solana.rs
|
||||
// version: 1
|
||||
// version: 3
|
||||
|
||||
//! `solana` decoder family.
|
||||
|
||||
pub mod core;
|
||||
mod core;
|
||||
|
||||
pub(crate) use self::core::SOLANA_CORE_ADDRESS_LOOKUP_TABLE_SURFACE_CODE;
|
||||
pub(crate) use self::core::SOLANA_CORE_BPF_LOADER_DEPRECATED_SURFACE_CODE;
|
||||
pub(crate) use self::core::SOLANA_CORE_BPF_LOADER_SURFACE_CODE;
|
||||
pub(crate) use self::core::SOLANA_CORE_BPF_LOADER_UPGRADEABLE_SURFACE_CODE;
|
||||
pub(crate) use self::core::SOLANA_CORE_COMPUTE_BUDGET_SURFACE_CODE;
|
||||
pub(crate) use self::core::SOLANA_CORE_CONFIG_SURFACE_CODE;
|
||||
pub(crate) use self::core::SOLANA_CORE_ED25519_PUBLIC_KEY_BYTES;
|
||||
pub(crate) use self::core::SOLANA_CORE_ED25519_SURFACE_CODE;
|
||||
pub(crate) use self::core::SOLANA_CORE_FEATURE_SURFACE_CODE;
|
||||
pub(crate) use self::core::SOLANA_CORE_LOADER_V4_SURFACE_CODE;
|
||||
pub(crate) use self::core::SOLANA_CORE_MAX_NATIVE_INSTRUCTION_PAYLOAD_BYTES;
|
||||
pub(crate) use self::core::SOLANA_CORE_NATIVE_EVENT_VERSION;
|
||||
pub(crate) use self::core::SOLANA_CORE_NATIVE_LOADER_SURFACE_CODE;
|
||||
pub(crate) use self::core::SOLANA_CORE_PRECOMPILE_COMPONENT_PREFIX_BYTES;
|
||||
pub(crate) use self::core::SOLANA_CORE_PROTOCOL_CODE;
|
||||
pub(crate) use self::core::SOLANA_CORE_SECP256K1_ETHEREUM_ADDRESS_BYTES;
|
||||
pub(crate) use self::core::SOLANA_CORE_SECP256K1_SURFACE_CODE;
|
||||
pub(crate) use self::core::SOLANA_CORE_SECP256R1_MAX_SIGNATURES;
|
||||
pub(crate) use self::core::SOLANA_CORE_SECP256R1_PUBLIC_KEY_BYTES;
|
||||
pub(crate) use self::core::SOLANA_CORE_SECP256R1_SURFACE_CODE;
|
||||
pub(crate) use self::core::SOLANA_CORE_SIGNATURE_BYTES;
|
||||
pub(crate) use self::core::SOLANA_CORE_SLASHING_SURFACE_CODE;
|
||||
pub(crate) use self::core::SOLANA_CORE_STAKE_SURFACE_CODE;
|
||||
pub(crate) use self::core::SOLANA_CORE_SYSTEM_SURFACE_CODE;
|
||||
pub(crate) use self::core::SOLANA_CORE_TRACING_TARGET;
|
||||
pub(crate) use self::core::SOLANA_CORE_U8_OFFSETS_BYTES;
|
||||
pub(crate) use self::core::SOLANA_CORE_U16_OFFSETS_BYTES;
|
||||
pub(crate) use self::core::SOLANA_CORE_VOTE_SURFACE_CODE;
|
||||
pub(crate) use self::core::SOLANA_CORE_ZK_ELGAMAL_PROOF_SURFACE_CODE;
|
||||
pub(crate) use self::core::SOLANA_CORE_ZK_TOKEN_PROOF_SURFACE_CODE;
|
||||
pub(crate) use self::core::SolanaCoreAccountRole;
|
||||
pub(crate) use self::core::SolanaCoreResolvedInstructionPayload;
|
||||
pub(crate) use self::core::solana_core_address_lookup_table_coverage;
|
||||
pub(crate) use self::core::solana_core_address_lookup_table_decode;
|
||||
pub(crate) use self::core::solana_core_address_lookup_table_recognize;
|
||||
pub(crate) use self::core::solana_core_bounded_hexadecimal_prefix;
|
||||
pub(crate) use self::core::solana_core_bounded_slice;
|
||||
pub(crate) use self::core::solana_core_compute_budget_coverage;
|
||||
pub(crate) use self::core::solana_core_compute_budget_decode;
|
||||
pub(crate) use self::core::solana_core_compute_budget_recognize;
|
||||
pub(crate) use self::core::solana_core_config_coverage;
|
||||
pub(crate) use self::core::solana_core_config_decode;
|
||||
pub(crate) use self::core::solana_core_config_recognize;
|
||||
pub(crate) use self::core::solana_core_decode_instruction_data;
|
||||
pub(crate) use self::core::solana_core_decoded_payload_length;
|
||||
pub(crate) use self::core::solana_core_decoded_payload_sha256;
|
||||
pub(crate) use self::core::solana_core_decoded_result;
|
||||
pub(crate) use self::core::solana_core_failed_result;
|
||||
pub(crate) use self::core::solana_core_feature_coverage;
|
||||
pub(crate) use self::core::solana_core_feature_decode;
|
||||
pub(crate) use self::core::solana_core_feature_recognize;
|
||||
pub(crate) use self::core::solana_core_hash_bytes;
|
||||
pub(crate) use self::core::solana_core_hexadecimal_prefix;
|
||||
pub(crate) use self::core::solana_core_loaders_coverage;
|
||||
pub(crate) use self::core::solana_core_loaders_decode;
|
||||
pub(crate) use self::core::solana_core_loaders_recognize;
|
||||
pub(crate) use self::core::solana_core_payload_hash;
|
||||
pub(crate) use self::core::solana_core_precompiles_coverage;
|
||||
pub(crate) use self::core::solana_core_precompiles_decode;
|
||||
pub(crate) use self::core::solana_core_precompiles_recognize;
|
||||
pub(crate) use self::core::solana_core_read_u32_le;
|
||||
pub(crate) use self::core::solana_core_read_u64_le;
|
||||
pub(crate) use self::core::solana_core_resolve_accounts;
|
||||
pub(crate) use self::core::solana_core_resolve_u8_instruction_payload;
|
||||
pub(crate) use self::core::solana_core_resolve_u16_instruction_payload;
|
||||
pub(crate) use self::core::solana_core_slashing_coverage;
|
||||
pub(crate) use self::core::solana_core_slashing_decode;
|
||||
pub(crate) use self::core::solana_core_slashing_recognize;
|
||||
pub(crate) use self::core::solana_core_stake_coverage;
|
||||
pub(crate) use self::core::solana_core_stake_decode;
|
||||
pub(crate) use self::core::solana_core_stake_recognize;
|
||||
pub(crate) use self::core::solana_core_system_coverage;
|
||||
pub(crate) use self::core::solana_core_system_decode;
|
||||
pub(crate) use self::core::solana_core_system_recognize;
|
||||
pub(crate) use self::core::solana_core_target_outer_instruction_index;
|
||||
pub(crate) use self::core::solana_core_unsupported_result;
|
||||
pub(crate) use self::core::solana_core_vote_coverage;
|
||||
pub(crate) use self::core::solana_core_vote_decode;
|
||||
pub(crate) use self::core::solana_core_vote_recognize;
|
||||
pub(crate) use self::core::solana_core_zk_elgamal_coverage;
|
||||
pub(crate) use self::core::solana_core_zk_elgamal_decode;
|
||||
pub(crate) use self::core::solana_core_zk_elgamal_recognize;
|
||||
pub(crate) use self::core::solana_core_zk_token_proof_coverage;
|
||||
pub(crate) use self::core::solana_core_zk_token_proof_decode;
|
||||
pub(crate) use self::core::solana_core_zk_token_proof_recognize;
|
||||
|
||||
/// Runtime-native Solana decoder with phased maximal native coverage.
|
||||
pub use self::core::SolanaCoreDecoder;
|
||||
|
||||
@@ -1,10 +1,197 @@
|
||||
// file: kb-lib/src/decoder/solana/core.rs
|
||||
// version: 1
|
||||
// version: 17
|
||||
|
||||
//! Migration boundary for legacy crate `kb_decoder_solana_core`.
|
||||
//! Solana Core decoder component.
|
||||
|
||||
/// Legacy crate name retained for migration and compatibility tracking.
|
||||
pub const LEGACY_CRATE: &str = "kb_decoder_solana_core";
|
||||
mod accounts;
|
||||
mod address_lookup_table;
|
||||
mod compute_budget;
|
||||
mod config;
|
||||
mod constants;
|
||||
mod decoder;
|
||||
mod event;
|
||||
mod feature;
|
||||
mod loaders;
|
||||
mod payload;
|
||||
mod precompiles;
|
||||
mod slashing;
|
||||
mod stake;
|
||||
mod system;
|
||||
mod vote;
|
||||
mod zk_elgamal;
|
||||
mod zk_token_proof;
|
||||
|
||||
/// Current porting status.
|
||||
pub const MIGRATION_STATUS: &str = "source-preserved-pending-port";
|
||||
/// Expected role and privileges for one positional instruction account.
|
||||
pub(crate) use self::accounts::AccountRole as SolanaCoreAccountRole;
|
||||
/// Resolves positional instruction accounts and validates their core indexes.
|
||||
pub(crate) use self::accounts::resolve_accounts as solana_core_resolve_accounts;
|
||||
/// Returns declared Address Lookup Table instruction coverage.
|
||||
pub(crate) use self::address_lookup_table::address_lookup_table_coverage as solana_core_address_lookup_table_coverage;
|
||||
/// Decodes one Address Lookup Table instruction.
|
||||
pub(crate) use self::address_lookup_table::address_lookup_table_decode as solana_core_address_lookup_table_decode;
|
||||
/// Recognizes one Address Lookup Table instruction without producing an event.
|
||||
pub(crate) use self::address_lookup_table::address_lookup_table_recognize as solana_core_address_lookup_table_recognize;
|
||||
/// Returns declared Compute Budget instruction coverage.
|
||||
pub(crate) use self::compute_budget::compute_budget_coverage as solana_core_compute_budget_coverage;
|
||||
/// Decodes one Compute Budget instruction.
|
||||
pub(crate) use self::compute_budget::compute_budget_decode as solana_core_compute_budget_decode;
|
||||
/// Recognizes one Compute Budget instruction without producing an event.
|
||||
pub(crate) use self::compute_budget::compute_budget_recognize as solana_core_compute_budget_recognize;
|
||||
/// Returns declared Config Program instruction coverage.
|
||||
pub(crate) use self::config::config_coverage as solana_core_config_coverage;
|
||||
/// Decodes one generic Config Program store instruction.
|
||||
pub(crate) use self::config::config_decode as solana_core_config_decode;
|
||||
/// Recognizes one generic Config Program store instruction.
|
||||
pub(crate) use self::config::config_recognize as solana_core_config_recognize;
|
||||
/// Stable Address Lookup Table surface code.
|
||||
pub(crate) use self::constants::ADDRESS_LOOKUP_TABLE_SURFACE_CODE as SOLANA_CORE_ADDRESS_LOOKUP_TABLE_SURFACE_CODE;
|
||||
/// Stable deprecated immutable BPF Loader surface code.
|
||||
pub(crate) use self::constants::BPF_LOADER_DEPRECATED_SURFACE_CODE as SOLANA_CORE_BPF_LOADER_DEPRECATED_SURFACE_CODE;
|
||||
/// Stable immutable BPF Loader v2 surface code.
|
||||
pub(crate) use self::constants::BPF_LOADER_SURFACE_CODE as SOLANA_CORE_BPF_LOADER_SURFACE_CODE;
|
||||
/// Stable upgradeable BPF Loader surface code.
|
||||
pub(crate) use self::constants::BPF_LOADER_UPGRADEABLE_SURFACE_CODE as SOLANA_CORE_BPF_LOADER_UPGRADEABLE_SURFACE_CODE;
|
||||
/// Stable Compute Budget surface code.
|
||||
pub(crate) use self::constants::COMPUTE_BUDGET_SURFACE_CODE as SOLANA_CORE_COMPUTE_BUDGET_SURFACE_CODE;
|
||||
/// Stable Config Program surface code.
|
||||
pub(crate) use self::constants::CONFIG_SURFACE_CODE as SOLANA_CORE_CONFIG_SURFACE_CODE;
|
||||
/// Byte length of an Ed25519 public key.
|
||||
pub(crate) use self::constants::ED25519_PUBLIC_KEY_BYTES as SOLANA_CORE_ED25519_PUBLIC_KEY_BYTES;
|
||||
/// Stable Ed25519 signature precompile surface code.
|
||||
pub(crate) use self::constants::ED25519_SURFACE_CODE as SOLANA_CORE_ED25519_SURFACE_CODE;
|
||||
/// Stable Feature Gate surface code.
|
||||
pub(crate) use self::constants::FEATURE_SURFACE_CODE as SOLANA_CORE_FEATURE_SURFACE_CODE;
|
||||
/// Stable Loader v4 surface code.
|
||||
pub(crate) use self::constants::LOADER_V4_SURFACE_CODE as SOLANA_CORE_LOADER_V4_SURFACE_CODE;
|
||||
/// Maximum retained native instruction payload accepted by the first decoder phase.
|
||||
pub(crate) use self::constants::MAX_NATIVE_INSTRUCTION_PAYLOAD_BYTES as SOLANA_CORE_MAX_NATIVE_INSTRUCTION_PAYLOAD_BYTES;
|
||||
/// Current native event payload contract version.
|
||||
pub(crate) use self::constants::NATIVE_EVENT_VERSION as SOLANA_CORE_NATIVE_EVENT_VERSION;
|
||||
/// Stable Native Loader surface code.
|
||||
pub(crate) use self::constants::NATIVE_LOADER_SURFACE_CODE as SOLANA_CORE_NATIVE_LOADER_SURFACE_CODE;
|
||||
/// Maximum number of component bytes retained as a hexadecimal event prefix.
|
||||
pub(crate) use self::constants::PRECOMPILE_COMPONENT_PREFIX_BYTES as SOLANA_CORE_PRECOMPILE_COMPONENT_PREFIX_BYTES;
|
||||
/// Stable protocol code shared by native Solana events.
|
||||
pub(crate) use self::constants::PROTOCOL_CODE as SOLANA_CORE_PROTOCOL_CODE;
|
||||
/// Byte length of a secp256k1 Ethereum address.
|
||||
pub(crate) use self::constants::SECP256K1_ETHEREUM_ADDRESS_BYTES as SOLANA_CORE_SECP256K1_ETHEREUM_ADDRESS_BYTES;
|
||||
/// Stable secp256k1 signature precompile surface code.
|
||||
pub(crate) use self::constants::SECP256K1_SURFACE_CODE as SOLANA_CORE_SECP256K1_SURFACE_CODE;
|
||||
/// Runtime maximum number of secp256r1 signatures in one precompile instruction.
|
||||
pub(crate) use self::constants::SECP256R1_MAX_SIGNATURES as SOLANA_CORE_SECP256R1_MAX_SIGNATURES;
|
||||
/// Byte length of a compressed secp256r1 public key.
|
||||
pub(crate) use self::constants::SECP256R1_PUBLIC_KEY_BYTES as SOLANA_CORE_SECP256R1_PUBLIC_KEY_BYTES;
|
||||
/// Stable secp256r1 signature precompile surface code.
|
||||
pub(crate) use self::constants::SECP256R1_SURFACE_CODE as SOLANA_CORE_SECP256R1_SURFACE_CODE;
|
||||
/// Byte length shared by compact Ed25519, secp256k1 and secp256r1 signatures.
|
||||
pub(crate) use self::constants::SIGNATURE_BYTES as SOLANA_CORE_SIGNATURE_BYTES;
|
||||
/// Stable Slashing Program decoder surface code.
|
||||
pub(crate) use self::constants::SLASHING_SURFACE_CODE as SOLANA_CORE_SLASHING_SURFACE_CODE;
|
||||
/// Stable Stake Program surface code.
|
||||
pub(crate) use self::constants::STAKE_SURFACE_CODE as SOLANA_CORE_STAKE_SURFACE_CODE;
|
||||
/// Stable System Program surface code.
|
||||
pub(crate) use self::constants::SYSTEM_SURFACE_CODE as SOLANA_CORE_SYSTEM_SURFACE_CODE;
|
||||
/// Canonical tracing target for the Solana Core decoder component.
|
||||
pub(crate) use self::constants::TRACING_TARGET as SOLANA_CORE_TRACING_TARGET;
|
||||
/// Byte length of one secp256k1 offsets entry.
|
||||
pub(crate) use self::constants::U8_OFFSETS_BYTES as SOLANA_CORE_U8_OFFSETS_BYTES;
|
||||
/// Byte length of one Ed25519 or secp256r1 offsets entry.
|
||||
pub(crate) use self::constants::U16_OFFSETS_BYTES as SOLANA_CORE_U16_OFFSETS_BYTES;
|
||||
/// Stable Vote Program surface code.
|
||||
pub(crate) use self::constants::VOTE_SURFACE_CODE as SOLANA_CORE_VOTE_SURFACE_CODE;
|
||||
/// Stable native ZK ElGamal Proof surface code.
|
||||
pub(crate) use self::constants::ZK_ELGAMAL_PROOF_SURFACE_CODE as SOLANA_CORE_ZK_ELGAMAL_PROOF_SURFACE_CODE;
|
||||
/// Stable historical ZK Token Proof surface code.
|
||||
pub(crate) use self::constants::ZK_TOKEN_PROOF_SURFACE_CODE as SOLANA_CORE_ZK_TOKEN_PROOF_SURFACE_CODE;
|
||||
/// Builds one exact decoded native observation.
|
||||
pub(crate) use self::event::decoded_result as solana_core_decoded_result;
|
||||
/// Builds one failed native decode result.
|
||||
pub(crate) use self::event::failed_result as solana_core_failed_result;
|
||||
/// Builds one unsupported native decode result with a bounded diagnostic.
|
||||
pub(crate) use self::event::unsupported_result as solana_core_unsupported_result;
|
||||
/// Returns declared Feature Gate instruction coverage.
|
||||
pub(crate) use self::feature::feature_coverage as solana_core_feature_coverage;
|
||||
/// Decodes one Feature Gate instruction.
|
||||
pub(crate) use self::feature::feature_decode as solana_core_feature_decode;
|
||||
/// Recognizes one Feature Gate instruction without producing an event.
|
||||
pub(crate) use self::feature::feature_recognize as solana_core_feature_recognize;
|
||||
/// Returns declared loader instruction coverage.
|
||||
pub(crate) use self::loaders::loaders_coverage as solana_core_loaders_coverage;
|
||||
/// Decodes one loader instruction.
|
||||
pub(crate) use self::loaders::loaders_decode as solana_core_loaders_decode;
|
||||
/// Recognizes one loader instruction without producing an event.
|
||||
pub(crate) use self::loaders::loaders_recognize as solana_core_loaders_recognize;
|
||||
/// One resolved outer instruction payload and its provenance relative to the target instruction.
|
||||
pub(crate) use self::payload::ResolvedInstructionPayload as SolanaCoreResolvedInstructionPayload;
|
||||
/// Returns a hexadecimal prefix no longer than the available byte slice.
|
||||
pub(crate) use self::payload::bounded_hexadecimal_prefix as solana_core_bounded_hexadecimal_prefix;
|
||||
/// Extracts one exact bounded byte slice with checked arithmetic.
|
||||
pub(crate) use self::payload::bounded_slice as solana_core_bounded_slice;
|
||||
/// Decodes one retained base64 instruction payload with an explicit byte limit.
|
||||
pub(crate) use self::payload::decode_instruction_data as solana_core_decode_instruction_data;
|
||||
/// Returns the decoded target payload length when retained and valid.
|
||||
pub(crate) use self::payload::decoded_payload_length as solana_core_decoded_payload_length;
|
||||
/// Returns the SHA-256 of the decoded target instruction data when available.
|
||||
pub(crate) use self::payload::decoded_payload_sha256 as solana_core_decoded_payload_sha256;
|
||||
/// Returns the lowercase SHA-256 of one bounded byte slice.
|
||||
pub(crate) use self::payload::hash_bytes as solana_core_hash_bytes;
|
||||
/// Returns a normalized lowercase hexadecimal prefix.
|
||||
pub(crate) use self::payload::hexadecimal_prefix as solana_core_hexadecimal_prefix;
|
||||
/// Returns a stable payload hash from core or computes it from retained JSON.
|
||||
pub(crate) use self::payload::payload_hash as solana_core_payload_hash;
|
||||
/// Reads one little-endian `u32` from an exact byte range.
|
||||
pub(crate) use self::payload::read_u32_le as solana_core_read_u32_le;
|
||||
/// Reads one little-endian `u64` from an exact byte range.
|
||||
pub(crate) use self::payload::read_u64_le as solana_core_read_u64_le;
|
||||
/// Resolves a secp256k1 instruction reference. The runtime format has no current-instruction
|
||||
/// sentinel: every `u8` value is an explicit outer instruction index.
|
||||
pub(crate) use self::payload::resolve_u8_instruction_payload as solana_core_resolve_u8_instruction_payload;
|
||||
/// Resolves an Ed25519 or secp256r1 instruction reference using the official `u16::MAX` sentinel.
|
||||
pub(crate) use self::payload::resolve_u16_instruction_payload as solana_core_resolve_u16_instruction_payload;
|
||||
/// Returns the numeric outer index of the target instruction.
|
||||
pub(crate) use self::payload::target_outer_instruction_index as solana_core_target_outer_instruction_index;
|
||||
/// Returns declared signature precompile coverage.
|
||||
pub(crate) use self::precompiles::precompiles_coverage as solana_core_precompiles_coverage;
|
||||
/// Decodes one signature precompile instruction structurally without cryptographic recomputation.
|
||||
pub(crate) use self::precompiles::precompiles_decode as solana_core_precompiles_decode;
|
||||
/// Recognizes one signature precompile instruction without resolving referenced data.
|
||||
pub(crate) use self::precompiles::precompiles_recognize as solana_core_precompiles_recognize;
|
||||
/// Returns declared Slashing Program instruction coverage.
|
||||
pub(crate) use self::slashing::slashing_coverage as solana_core_slashing_coverage;
|
||||
/// Decodes one Slashing Program instruction.
|
||||
pub(crate) use self::slashing::slashing_decode as solana_core_slashing_decode;
|
||||
/// Recognizes one Slashing Program instruction without producing an event.
|
||||
pub(crate) use self::slashing::slashing_recognize as solana_core_slashing_recognize;
|
||||
/// Returns declared Stake Program instruction coverage.
|
||||
pub(crate) use self::stake::stake_coverage as solana_core_stake_coverage;
|
||||
/// Decodes one Stake Program instruction.
|
||||
pub(crate) use self::stake::stake_decode as solana_core_stake_decode;
|
||||
/// Recognizes one Stake Program instruction without producing an event.
|
||||
pub(crate) use self::stake::stake_recognize as solana_core_stake_recognize;
|
||||
/// Returns declared System Program instruction coverage.
|
||||
pub(crate) use self::system::system_coverage as solana_core_system_coverage;
|
||||
/// Decodes one System Program instruction.
|
||||
pub(crate) use self::system::system_decode as solana_core_system_decode;
|
||||
/// Recognizes one System Program instruction without producing an event.
|
||||
pub(crate) use self::system::system_recognize as solana_core_system_recognize;
|
||||
/// Returns declared Vote Program instruction coverage.
|
||||
pub(crate) use self::vote::vote_coverage as solana_core_vote_coverage;
|
||||
/// Decodes one Vote Program instruction.
|
||||
pub(crate) use self::vote::vote_decode as solana_core_vote_decode;
|
||||
/// Recognizes one Vote Program instruction without producing an event.
|
||||
pub(crate) use self::vote::vote_recognize as solana_core_vote_recognize;
|
||||
/// Returns declared ZK ElGamal Proof instruction coverage.
|
||||
pub(crate) use self::zk_elgamal::zk_elgamal_coverage as solana_core_zk_elgamal_coverage;
|
||||
/// Decodes one native ZK ElGamal Proof instruction without recomputing the proof.
|
||||
pub(crate) use self::zk_elgamal::zk_elgamal_decode as solana_core_zk_elgamal_decode;
|
||||
/// Recognizes one native ZK ElGamal Proof instruction.
|
||||
pub(crate) use self::zk_elgamal::zk_elgamal_recognize as solana_core_zk_elgamal_recognize;
|
||||
/// Returns declared historical and current ZK Token Proof coverage.
|
||||
pub(crate) use self::zk_token_proof::zk_token_proof_coverage as solana_core_zk_token_proof_coverage;
|
||||
/// Decodes one historical ZK Token Proof layout without recomputing any proof.
|
||||
pub(crate) use self::zk_token_proof::zk_token_proof_decode as solana_core_zk_token_proof_decode;
|
||||
/// Recognizes one historical ZK Token Proof layout or the current no-op runtime fallback.
|
||||
pub(crate) use self::zk_token_proof::zk_token_proof_recognize as solana_core_zk_token_proof_recognize;
|
||||
|
||||
/// Runtime-native Solana decoder with phased maximal native coverage.
|
||||
pub use self::decoder::SolanaCoreDecoder;
|
||||
|
||||
137
kb-lib/src/decoder/solana/core/accounts.rs
Normal file
137
kb-lib/src/decoder/solana/core/accounts.rs
Normal file
@@ -0,0 +1,137 @@
|
||||
// file: kb-lib/src/decoder/solana/core/accounts.rs
|
||||
// version: 4
|
||||
|
||||
//! Resolved account-role validation for native instructions.
|
||||
|
||||
/// Expected role and privileges for one positional instruction account.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct AccountRole {
|
||||
/// Stable role code.
|
||||
pub(crate) role: &'static str,
|
||||
/// Whether the official instruction contract marks the account writable.
|
||||
pub(crate) expected_writable: bool,
|
||||
/// Whether the official instruction contract marks the account as a signer.
|
||||
pub(crate) expected_signer: std::option::Option<bool>,
|
||||
}
|
||||
|
||||
impl AccountRole {
|
||||
/// Builds one account role declaration.
|
||||
pub(crate) const fn new(
|
||||
role: &'static str,
|
||||
expected_writable: bool,
|
||||
expected_signer: bool,
|
||||
) -> Self {
|
||||
return Self {
|
||||
role,
|
||||
expected_writable,
|
||||
expected_signer: std::option::Option::Some(expected_signer),
|
||||
};
|
||||
}
|
||||
|
||||
/// Builds one role whose signer requirement differs across supported runtime generations.
|
||||
pub(crate) const fn optional_signer(role: &'static str, expected_writable: bool) -> Self {
|
||||
return Self {
|
||||
role,
|
||||
expected_writable,
|
||||
expected_signer: std::option::Option::None,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves positional instruction accounts and validates their core indexes.
|
||||
pub(crate) fn resolve_accounts(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
roles: &[crate::SolanaCoreAccountRole],
|
||||
minimum_count: usize,
|
||||
maximum_count: std::option::Option<usize>,
|
||||
) -> kb_core::Result<serde_json::Value> {
|
||||
let instruction_accounts = match input.instruction_accounts_json.as_array() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(
|
||||
"native instruction accounts must be a JSON array",
|
||||
));
|
||||
},
|
||||
};
|
||||
if instruction_accounts.len() < minimum_count {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||
"native instruction requires at least {minimum_count} accounts but received {}",
|
||||
instruction_accounts.len()
|
||||
)));
|
||||
}
|
||||
if maximum_count.is_some_and(|value| return instruction_accounts.len() > value) {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||
"native instruction accepts at most {} accounts but received {}",
|
||||
maximum_count.unwrap_or(0),
|
||||
instruction_accounts.len()
|
||||
)));
|
||||
}
|
||||
let account_keys = match input.account_keys_json.as_array() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(
|
||||
"native transaction account keys must be a JSON array",
|
||||
));
|
||||
},
|
||||
};
|
||||
let mut output = std::vec::Vec::with_capacity(instruction_accounts.len());
|
||||
for (position, instruction_account) in instruction_accounts.iter().enumerate() {
|
||||
let account_index =
|
||||
match instruction_account.get("accountIndex").and_then(serde_json::Value::as_u64) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||
"native instruction account {position} has no valid accountIndex"
|
||||
)));
|
||||
},
|
||||
};
|
||||
let instruction_key =
|
||||
match instruction_account.get("accountKey").and_then(serde_json::Value::as_str) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||
"native instruction account {position} has no accountKey"
|
||||
)));
|
||||
},
|
||||
};
|
||||
let resolved = account_keys.iter().find(|candidate| {
|
||||
return candidate.get("accountIndex").and_then(serde_json::Value::as_u64)
|
||||
== std::option::Option::Some(account_index);
|
||||
});
|
||||
let resolved = match resolved {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||
"native instruction account index {account_index} is absent from resolved keys"
|
||||
)));
|
||||
},
|
||||
};
|
||||
let resolved_key = match resolved.get("accountKey").and_then(serde_json::Value::as_str) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||
"resolved native account index {account_index} has no accountKey"
|
||||
)));
|
||||
},
|
||||
};
|
||||
if resolved_key != instruction_key {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||
"native instruction account index {account_index} resolves to a different key"
|
||||
)));
|
||||
}
|
||||
let role = roles.get(position);
|
||||
output.push(serde_json::json!({
|
||||
"position": position,
|
||||
"accountIndex": account_index,
|
||||
"accountKey": instruction_key,
|
||||
"role": role.map(|value| return value.role).unwrap_or("additional_account"),
|
||||
"expectedWritable": role.map(|value| return value.expected_writable),
|
||||
"expectedSigner": role.map(|value| return value.expected_signer),
|
||||
"writable": resolved.get("writable").and_then(serde_json::Value::as_bool),
|
||||
"signer": resolved.get("signer").and_then(serde_json::Value::as_bool),
|
||||
"executable": resolved.get("executable").and_then(serde_json::Value::as_bool),
|
||||
"source": resolved.get("source").cloned().unwrap_or(serde_json::Value::Null),
|
||||
}));
|
||||
}
|
||||
return std::result::Result::Ok(serde_json::Value::Array(output));
|
||||
}
|
||||
505
kb-lib/src/decoder/solana/core/address_lookup_table.rs
Normal file
505
kb-lib/src/decoder/solana/core/address_lookup_table.rs
Normal file
@@ -0,0 +1,505 @@
|
||||
// file: kb-lib/src/decoder/solana/core/address_lookup_table.rs
|
||||
// version: 5
|
||||
|
||||
//! Exact Address Lookup Table instruction decoding through the official interface schema.
|
||||
|
||||
const SOURCE: &str =
|
||||
"solana-address-lookup-table-interface@3.1.0 ProgramInstruction wincode contract";
|
||||
|
||||
const CREATE_ROLES: &[crate::SolanaCoreAccountRole] = &[
|
||||
crate::SolanaCoreAccountRole::new("lookup_table", true, false),
|
||||
crate::SolanaCoreAccountRole::optional_signer("authority", false),
|
||||
crate::SolanaCoreAccountRole::new("payer", true, true),
|
||||
crate::SolanaCoreAccountRole::new("system_program", false, false),
|
||||
];
|
||||
const AUTHORITY_ROLES: &[crate::SolanaCoreAccountRole] = &[
|
||||
crate::SolanaCoreAccountRole::new("lookup_table", true, false),
|
||||
crate::SolanaCoreAccountRole::new("authority", false, true),
|
||||
];
|
||||
const EXTEND_ROLES: &[crate::SolanaCoreAccountRole] = &[
|
||||
crate::SolanaCoreAccountRole::new("lookup_table", true, false),
|
||||
crate::SolanaCoreAccountRole::new("authority", false, true),
|
||||
crate::SolanaCoreAccountRole::new("payer", true, true),
|
||||
crate::SolanaCoreAccountRole::new("system_program", false, false),
|
||||
];
|
||||
const CLOSE_ROLES: &[crate::SolanaCoreAccountRole] = &[
|
||||
crate::SolanaCoreAccountRole::new("lookup_table", true, false),
|
||||
crate::SolanaCoreAccountRole::new("authority", false, true),
|
||||
crate::SolanaCoreAccountRole::new("recipient", true, false),
|
||||
];
|
||||
|
||||
/// Returns declared Address Lookup Table instruction coverage.
|
||||
pub(crate) fn address_lookup_table_coverage() -> std::vec::Vec<crate::DecoderCoverageDeclaration> {
|
||||
return vec![
|
||||
coverage_entry(
|
||||
"create_lookup_table",
|
||||
solana_address_lookup_table_interface::instruction::ProgramInstruction::CreateLookupTable {
|
||||
recent_slot: 0,
|
||||
bump_seed: 0,
|
||||
},
|
||||
),
|
||||
coverage_entry(
|
||||
"freeze_lookup_table",
|
||||
solana_address_lookup_table_interface::instruction::ProgramInstruction::FreezeLookupTable,
|
||||
),
|
||||
coverage_entry(
|
||||
"extend_lookup_table",
|
||||
solana_address_lookup_table_interface::instruction::ProgramInstruction::ExtendLookupTable {
|
||||
new_addresses: std::vec::Vec::new(),
|
||||
},
|
||||
),
|
||||
coverage_entry(
|
||||
"deactivate_lookup_table",
|
||||
solana_address_lookup_table_interface::instruction::ProgramInstruction::DeactivateLookupTable,
|
||||
),
|
||||
coverage_entry(
|
||||
"close_lookup_table",
|
||||
solana_address_lookup_table_interface::instruction::ProgramInstruction::CloseLookupTable,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/// Recognizes one Address Lookup Table instruction without producing an event.
|
||||
pub(crate) fn address_lookup_table_recognize(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
priority: u16,
|
||||
) -> crate::DecoderRecognition {
|
||||
let bytes_result = crate::solana_core_decode_instruction_data(input);
|
||||
let bytes = match bytes_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_error) => {
|
||||
return crate::DecoderRecognition::compatible(
|
||||
false,
|
||||
priority,
|
||||
std::option::Option::Some(
|
||||
crate::SOLANA_CORE_ADDRESS_LOOKUP_TABLE_SURFACE_CODE.to_string(),
|
||||
),
|
||||
std::option::Option::Some("malformed_address_lookup_table_instruction".to_string()),
|
||||
std::option::Option::None,
|
||||
);
|
||||
},
|
||||
};
|
||||
let tag_result = crate::solana_core_read_u32_le(bytes.as_slice(), 0);
|
||||
let tag = match tag_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_error) => {
|
||||
return crate::DecoderRecognition::compatible(
|
||||
false,
|
||||
priority,
|
||||
std::option::Option::Some(
|
||||
crate::SOLANA_CORE_ADDRESS_LOOKUP_TABLE_SURFACE_CODE.to_string(),
|
||||
),
|
||||
std::option::Option::Some("malformed_address_lookup_table_instruction".to_string()),
|
||||
crate::solana_core_hexadecimal_prefix(bytes.as_slice(), bytes.len().min(4)),
|
||||
);
|
||||
},
|
||||
};
|
||||
let entry_code = entry_code(tag);
|
||||
let exact = if entry_code.is_some() {
|
||||
wincode::deserialize_exact::<
|
||||
solana_address_lookup_table_interface::instruction::ProgramInstruction,
|
||||
>(bytes.as_slice())
|
||||
.is_ok()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
return crate::DecoderRecognition::compatible(
|
||||
exact,
|
||||
priority,
|
||||
std::option::Option::Some(crate::SOLANA_CORE_ADDRESS_LOOKUP_TABLE_SURFACE_CODE.to_string()),
|
||||
entry_code.map(str::to_string).or_else(|| {
|
||||
return std::option::Option::Some(
|
||||
"unknown_address_lookup_table_instruction".to_string(),
|
||||
);
|
||||
}),
|
||||
crate::solana_core_hexadecimal_prefix(bytes.as_slice(), 4),
|
||||
);
|
||||
}
|
||||
|
||||
/// Decodes one Address Lookup Table instruction.
|
||||
pub(crate) fn address_lookup_table_decode(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
) -> crate::DecoderExecutionResult {
|
||||
let bytes_result = crate::solana_core_decode_instruction_data(input);
|
||||
let bytes = match bytes_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some("malformed_address_lookup_table_instruction"),
|
||||
"address_lookup_table_payload_invalid",
|
||||
error.to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
let tag_result = crate::solana_core_read_u32_le(bytes.as_slice(), 0);
|
||||
let tag = match tag_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some("malformed_address_lookup_table_instruction"),
|
||||
"address_lookup_table_tag_truncated",
|
||||
error.to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
let entry_code = match entry_code(tag) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return crate::solana_core_unsupported_result(
|
||||
"unknown_address_lookup_table_instruction",
|
||||
"address_lookup_table_tag_unknown",
|
||||
format!(
|
||||
"unknown Address Lookup Table instruction tag {tag}; payload_sha256={}",
|
||||
crate::solana_core_payload_hash(input)
|
||||
),
|
||||
);
|
||||
},
|
||||
};
|
||||
let instruction_result = wincode::deserialize_exact::<
|
||||
solana_address_lookup_table_interface::instruction::ProgramInstruction,
|
||||
>(bytes.as_slice());
|
||||
let instruction = match instruction_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some(entry_code),
|
||||
"address_lookup_table_wincode_invalid",
|
||||
format!("cannot decode exact Address Lookup Table {entry_code}: {error}"),
|
||||
);
|
||||
},
|
||||
};
|
||||
return decoded_instruction(input, instruction);
|
||||
}
|
||||
|
||||
fn decoded_instruction(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
instruction: solana_address_lookup_table_interface::instruction::ProgramInstruction,
|
||||
) -> crate::DecoderExecutionResult {
|
||||
return match instruction {
|
||||
solana_address_lookup_table_interface::instruction::ProgramInstruction::CreateLookupTable {
|
||||
recent_slot,
|
||||
bump_seed,
|
||||
} => build(
|
||||
input,
|
||||
"create_lookup_table",
|
||||
CREATE_ROLES,
|
||||
4,
|
||||
std::option::Option::Some(4),
|
||||
serde_json::json!({
|
||||
"recentSlot": recent_slot,
|
||||
"bumpSeed": bump_seed,
|
||||
"authoritySignerPolicy": "optional_current_historical_required",
|
||||
}),
|
||||
),
|
||||
solana_address_lookup_table_interface::instruction::ProgramInstruction::FreezeLookupTable => {
|
||||
build(
|
||||
input,
|
||||
"freeze_lookup_table",
|
||||
AUTHORITY_ROLES,
|
||||
2,
|
||||
std::option::Option::Some(2),
|
||||
serde_json::json!({}),
|
||||
)
|
||||
},
|
||||
solana_address_lookup_table_interface::instruction::ProgramInstruction::ExtendLookupTable {
|
||||
new_addresses,
|
||||
} => {
|
||||
let account_count = input
|
||||
.instruction_accounts_json
|
||||
.as_array()
|
||||
.map(std::vec::Vec::len)
|
||||
.unwrap_or(0);
|
||||
if account_count != 2 && account_count != 4 {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some("extend_lookup_table"),
|
||||
"address_lookup_table_extend_accounts_invalid",
|
||||
format!(
|
||||
"Address Lookup Table extend requires 2 accounts or the complete optional payer/system pair of 4 accounts but received {account_count}"
|
||||
),
|
||||
);
|
||||
}
|
||||
let addresses = new_addresses
|
||||
.iter()
|
||||
.map(std::string::ToString::to_string)
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
build(
|
||||
input,
|
||||
"extend_lookup_table",
|
||||
EXTEND_ROLES,
|
||||
2,
|
||||
std::option::Option::Some(4),
|
||||
serde_json::json!({
|
||||
"newAddresses": addresses,
|
||||
"newAddressCount": new_addresses.len(),
|
||||
"fundingAccountsPresent": account_count == 4,
|
||||
}),
|
||||
)
|
||||
},
|
||||
solana_address_lookup_table_interface::instruction::ProgramInstruction::DeactivateLookupTable => {
|
||||
build(
|
||||
input,
|
||||
"deactivate_lookup_table",
|
||||
AUTHORITY_ROLES,
|
||||
2,
|
||||
std::option::Option::Some(2),
|
||||
serde_json::json!({}),
|
||||
)
|
||||
},
|
||||
solana_address_lookup_table_interface::instruction::ProgramInstruction::CloseLookupTable => {
|
||||
build(
|
||||
input,
|
||||
"close_lookup_table",
|
||||
CLOSE_ROLES,
|
||||
3,
|
||||
std::option::Option::Some(3),
|
||||
serde_json::json!({}),
|
||||
)
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn build(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
entry_code: &str,
|
||||
roles: &[crate::SolanaCoreAccountRole],
|
||||
minimum_count: usize,
|
||||
maximum_count: std::option::Option<usize>,
|
||||
parameters: serde_json::Value,
|
||||
) -> crate::DecoderExecutionResult {
|
||||
let accounts_result =
|
||||
crate::solana_core_resolve_accounts(input, roles, minimum_count, maximum_count);
|
||||
let accounts = match accounts_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some(entry_code),
|
||||
"address_lookup_table_accounts_invalid",
|
||||
error.to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
return crate::solana_core_decoded_result(
|
||||
input,
|
||||
crate::SOLANA_CORE_ADDRESS_LOOKUP_TABLE_SURFACE_CODE,
|
||||
entry_code,
|
||||
crate::EventFamily::Lifecycle,
|
||||
false,
|
||||
accounts,
|
||||
parameters,
|
||||
SOURCE,
|
||||
);
|
||||
}
|
||||
|
||||
fn entry_code(tag: u32) -> std::option::Option<&'static str> {
|
||||
return match tag {
|
||||
0 => std::option::Option::Some("create_lookup_table"),
|
||||
1 => std::option::Option::Some("freeze_lookup_table"),
|
||||
2 => std::option::Option::Some("extend_lookup_table"),
|
||||
3 => std::option::Option::Some("deactivate_lookup_table"),
|
||||
4 => std::option::Option::Some("close_lookup_table"),
|
||||
_ => std::option::Option::None,
|
||||
};
|
||||
}
|
||||
|
||||
fn coverage_entry(
|
||||
entry_code: &str,
|
||||
instruction: solana_address_lookup_table_interface::instruction::ProgramInstruction,
|
||||
) -> crate::DecoderCoverageDeclaration {
|
||||
let discriminator_hex = match wincode::serialize(&instruction) {
|
||||
std::result::Result::Ok(bytes) => {
|
||||
crate::solana_core_hexadecimal_prefix(bytes.as_slice(), 4)
|
||||
},
|
||||
std::result::Result::Err(_error) => std::option::Option::None,
|
||||
};
|
||||
return crate::DecoderCoverageDeclaration {
|
||||
program_id: kb_program_ids::ADDRESS_LOOKUP_TABLE_PROGRAM_ID.to_string(),
|
||||
surface_code: std::option::Option::Some(
|
||||
crate::SOLANA_CORE_ADDRESS_LOOKUP_TABLE_SURFACE_CODE.to_string(),
|
||||
),
|
||||
entry_kind: crate::DecoderCoverageEntryKind::Instruction,
|
||||
entry_code: entry_code.to_string(),
|
||||
discriminator_hex,
|
||||
historical: false,
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use base64::Engine; // rust-rules: trait-import
|
||||
|
||||
fn replay_input(
|
||||
instruction: &solana_address_lookup_table_interface::instruction::ProgramInstruction,
|
||||
account_count: usize,
|
||||
transaction_failed: bool,
|
||||
) -> crate::CoreInstructionReplayInput {
|
||||
let bytes_result = wincode::serialize(instruction);
|
||||
let bytes = match bytes_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("ALT fixture serialization failed: {error}"),
|
||||
};
|
||||
return replay_input_bytes(bytes.as_slice(), account_count, transaction_failed);
|
||||
}
|
||||
|
||||
fn replay_input_bytes(
|
||||
bytes: &[u8],
|
||||
account_count: usize,
|
||||
transaction_failed: bool,
|
||||
) -> crate::CoreInstructionReplayInput {
|
||||
let encoded = base64::engine::general_purpose::STANDARD.encode(bytes);
|
||||
let account_keys = (0..account_count)
|
||||
.map(|index| {
|
||||
return serde_json::json!({
|
||||
"accountIndex": index,
|
||||
"accountKey": format!("account{index}"),
|
||||
"source": "static",
|
||||
"writable": true,
|
||||
"signer": true,
|
||||
"executable": false,
|
||||
});
|
||||
})
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
let instruction_accounts = (0..account_count)
|
||||
.map(|index| {
|
||||
return serde_json::json!({
|
||||
"accountIndex": index,
|
||||
"accountKey": format!("account{index}"),
|
||||
});
|
||||
})
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
let result = crate::CoreInstructionReplayInput::new(
|
||||
"signature:0",
|
||||
"signature",
|
||||
42,
|
||||
"0",
|
||||
kb_program_ids::ADDRESS_LOOKUP_TABLE_PROGRAM_ID,
|
||||
transaction_failed,
|
||||
if transaction_failed {
|
||||
std::option::Option::Some(serde_json::json!({"InstructionError": [0, "Custom"]}))
|
||||
} else {
|
||||
std::option::Option::None
|
||||
},
|
||||
serde_json::Value::Array(account_keys),
|
||||
serde_json::Value::Array(instruction_accounts),
|
||||
std::option::Option::Some(serde_json::json!({"dataBase64": encoded})),
|
||||
std::option::Option::Some("payload-hash".to_string()),
|
||||
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!("ALT replay input failed: {error}"),
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_official_variant_decodes() {
|
||||
let address = solana_sdk_ids::address_lookup_table::id();
|
||||
let fixtures = [
|
||||
(
|
||||
solana_address_lookup_table_interface::instruction::ProgramInstruction::CreateLookupTable {
|
||||
recent_slot: u64::MAX,
|
||||
bump_seed: u8::MAX,
|
||||
},
|
||||
4,
|
||||
"create_lookup_table",
|
||||
),
|
||||
(
|
||||
solana_address_lookup_table_interface::instruction::ProgramInstruction::FreezeLookupTable,
|
||||
2,
|
||||
"freeze_lookup_table",
|
||||
),
|
||||
(
|
||||
solana_address_lookup_table_interface::instruction::ProgramInstruction::ExtendLookupTable {
|
||||
new_addresses: vec![address, address],
|
||||
},
|
||||
4,
|
||||
"extend_lookup_table",
|
||||
),
|
||||
(
|
||||
solana_address_lookup_table_interface::instruction::ProgramInstruction::DeactivateLookupTable,
|
||||
2,
|
||||
"deactivate_lookup_table",
|
||||
),
|
||||
(
|
||||
solana_address_lookup_table_interface::instruction::ProgramInstruction::CloseLookupTable,
|
||||
3,
|
||||
"close_lookup_table",
|
||||
),
|
||||
];
|
||||
for (instruction, account_count, expected) in fixtures {
|
||||
let result = crate::solana_core_address_lookup_table_decode(&replay_input(
|
||||
&instruction,
|
||||
account_count,
|
||||
false,
|
||||
));
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
assert_eq!(
|
||||
result.recognized_entry_code.as_deref(),
|
||||
std::option::Option::Some(expected)
|
||||
);
|
||||
assert_eq!(result.observations.len(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extend_accepts_only_absent_or_complete_funding_pair() {
|
||||
let instruction = solana_address_lookup_table_interface::instruction::ProgramInstruction::ExtendLookupTable {
|
||||
new_addresses: std::vec::Vec::new(),
|
||||
};
|
||||
let without_funding =
|
||||
crate::solana_core_address_lookup_table_decode(&replay_input(&instruction, 2, false));
|
||||
assert_eq!(without_funding.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
let incomplete =
|
||||
crate::solana_core_address_lookup_table_decode(&replay_input(&instruction, 3, false));
|
||||
assert_eq!(incomplete.status, crate::DecoderOutcomeStatus::Failed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncated_unknown_and_trailing_payloads_are_distinguished() {
|
||||
let truncated = crate::solana_core_address_lookup_table_decode(&replay_input_bytes(
|
||||
&[0, 0, 0],
|
||||
4,
|
||||
false,
|
||||
));
|
||||
assert_eq!(truncated.status, crate::DecoderOutcomeStatus::Failed);
|
||||
let unknown = crate::solana_core_address_lookup_table_decode(&replay_input_bytes(
|
||||
&[255, 0, 0, 0],
|
||||
0,
|
||||
false,
|
||||
));
|
||||
assert_eq!(unknown.status, crate::DecoderOutcomeStatus::Unsupported);
|
||||
let mut valid = match wincode::serialize(
|
||||
&solana_address_lookup_table_interface::instruction::ProgramInstruction::FreezeLookupTable,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("ALT fixture serialization failed: {error}"),
|
||||
};
|
||||
valid.push(1);
|
||||
let trailing = crate::solana_core_address_lookup_table_decode(&replay_input_bytes(
|
||||
valid.as_slice(),
|
||||
2,
|
||||
false,
|
||||
));
|
||||
assert_eq!(trailing.status, crate::DecoderOutcomeStatus::Failed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_transaction_is_decoded_as_uncommitted_lifecycle_intent() {
|
||||
let instruction = solana_address_lookup_table_interface::instruction::ProgramInstruction::DeactivateLookupTable;
|
||||
let result =
|
||||
crate::solana_core_address_lookup_table_decode(&replay_input(&instruction, 2, true));
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
assert!(!result.observations[0].observation_committed);
|
||||
assert_eq!(result.observations[0].payload_json["transactionSucceeded"], false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coverage_uses_official_encoder_discriminants() {
|
||||
let coverage = crate::solana_core_address_lookup_table_coverage();
|
||||
assert_eq!(coverage.len(), 5);
|
||||
assert_eq!(coverage[0].discriminator_hex.as_deref(), std::option::Option::Some("00000000"));
|
||||
assert_eq!(coverage[4].discriminator_hex.as_deref(), std::option::Option::Some("04000000"));
|
||||
}
|
||||
}
|
||||
896
kb-lib/src/decoder/solana/core/compute_budget.rs
Normal file
896
kb-lib/src/decoder/solana/core/compute_budget.rs
Normal file
@@ -0,0 +1,896 @@
|
||||
// file: kb-lib/src/decoder/solana/core/compute_budget.rs
|
||||
// version: 6
|
||||
|
||||
//! Maximal current and historical Compute Budget instruction decoding.
|
||||
|
||||
use base64::Engine; // rust-rules: trait-import
|
||||
|
||||
const SOURCE_CURRENT: &str = "solana-compute-budget-interface@3.0.0; solana-compute-budget-instruction@4.1.1::try_from_slice_unchecked";
|
||||
const SOURCE_HISTORICAL: &str =
|
||||
"solana-sdk compute_budget::ComputeBudgetInstruction RequestUnitsDeprecated";
|
||||
|
||||
/// Returns declared Compute Budget instruction coverage.
|
||||
pub(crate) fn compute_budget_coverage() -> std::vec::Vec<crate::DecoderCoverageDeclaration> {
|
||||
return vec![
|
||||
coverage_entry("unused_reserved", "00", false),
|
||||
coverage_entry("request_units_deprecated", "00", true),
|
||||
coverage_entry("request_heap_frame", "01", false),
|
||||
coverage_entry("set_compute_unit_limit", "02", false),
|
||||
coverage_entry("set_compute_unit_price", "03", false),
|
||||
coverage_entry("set_loaded_accounts_data_size_limit", "04", false),
|
||||
];
|
||||
}
|
||||
|
||||
/// Recognizes one Compute Budget instruction without producing an event.
|
||||
pub(crate) fn compute_budget_recognize(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
priority: u16,
|
||||
) -> crate::DecoderRecognition {
|
||||
let bytes_result = crate::solana_core_decode_instruction_data(input);
|
||||
let bytes = match bytes_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_error) => {
|
||||
return crate::DecoderRecognition::compatible(
|
||||
false,
|
||||
priority,
|
||||
std::option::Option::Some(
|
||||
crate::SOLANA_CORE_COMPUTE_BUDGET_SURFACE_CODE.to_string(),
|
||||
),
|
||||
std::option::Option::Some("malformed_compute_budget_instruction".to_string()),
|
||||
std::option::Option::None,
|
||||
);
|
||||
},
|
||||
};
|
||||
let entry_code = entry_code(bytes.as_slice());
|
||||
return crate::DecoderRecognition::compatible(
|
||||
entry_code.is_some(),
|
||||
priority,
|
||||
std::option::Option::Some(crate::SOLANA_CORE_COMPUTE_BUDGET_SURFACE_CODE.to_string()),
|
||||
entry_code.map(str::to_string).or_else(|| {
|
||||
return std::option::Option::Some("unknown_compute_budget_instruction".to_string());
|
||||
}),
|
||||
crate::solana_core_hexadecimal_prefix(bytes.as_slice(), 1),
|
||||
);
|
||||
}
|
||||
|
||||
/// Decodes one Compute Budget instruction.
|
||||
pub(crate) fn compute_budget_decode(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
) -> crate::DecoderExecutionResult {
|
||||
let bytes_result = crate::solana_core_decode_instruction_data(input);
|
||||
let bytes = match bytes_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some("malformed_compute_budget_instruction"),
|
||||
"compute_budget_payload_invalid",
|
||||
error.to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
if bytes.is_empty() {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some("malformed_compute_budget_instruction"),
|
||||
"compute_budget_payload_empty",
|
||||
"Compute Budget instruction payload is empty",
|
||||
);
|
||||
}
|
||||
let accounts_result =
|
||||
crate::solana_core_resolve_accounts(input, &[], 0, std::option::Option::None);
|
||||
let accounts = match accounts_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return crate::solana_core_failed_result(
|
||||
entry_code(bytes.as_slice()),
|
||||
"compute_budget_accounts_invalid",
|
||||
error.to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
return match bytes[0] {
|
||||
0 if bytes.len() == 1 => crate::DecoderExecutionResult::ignored(std::option::Option::Some(
|
||||
"unused_reserved".to_string(),
|
||||
)),
|
||||
0 if bytes.len() == 9 => {
|
||||
let units_result = crate::solana_core_read_u32_le(bytes.as_slice(), 1);
|
||||
let units = match units_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some("request_units_deprecated"),
|
||||
"compute_budget_request_units_truncated",
|
||||
error.to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
let additional_fee_result = crate::solana_core_read_u32_le(bytes.as_slice(), 5);
|
||||
let additional_fee = match additional_fee_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some("request_units_deprecated"),
|
||||
"compute_budget_request_units_truncated",
|
||||
error.to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
let mut parameters = serde_json::Map::new();
|
||||
parameters.insert("units".to_string(), serde_json::json!(units));
|
||||
parameters.insert("additionalFee".to_string(), serde_json::json!(additional_fee));
|
||||
append_transaction_profile(input, &mut parameters);
|
||||
crate::solana_core_decoded_result(
|
||||
input,
|
||||
crate::SOLANA_CORE_COMPUTE_BUDGET_SURFACE_CODE,
|
||||
"request_units_deprecated",
|
||||
crate::EventFamily::Audit,
|
||||
true,
|
||||
accounts,
|
||||
serde_json::Value::Object(parameters),
|
||||
SOURCE_HISTORICAL,
|
||||
)
|
||||
},
|
||||
0 => crate::solana_core_failed_result(
|
||||
std::option::Option::Some("request_units_deprecated"),
|
||||
"compute_budget_request_units_size_invalid",
|
||||
format!(
|
||||
"historical RequestUnitsDeprecated requires 9 bytes but received {}",
|
||||
bytes.len()
|
||||
),
|
||||
),
|
||||
1 => {
|
||||
decode_u32_instruction(input, bytes.as_slice(), accounts, "request_heap_frame", "bytes")
|
||||
},
|
||||
2 => decode_u32_instruction(
|
||||
input,
|
||||
bytes.as_slice(),
|
||||
accounts,
|
||||
"set_compute_unit_limit",
|
||||
"computeUnitLimit",
|
||||
),
|
||||
3 => decode_u64_instruction(
|
||||
input,
|
||||
bytes.as_slice(),
|
||||
accounts,
|
||||
"set_compute_unit_price",
|
||||
"microLamports",
|
||||
),
|
||||
4 => decode_u32_instruction(
|
||||
input,
|
||||
bytes.as_slice(),
|
||||
accounts,
|
||||
"set_loaded_accounts_data_size_limit",
|
||||
"bytes",
|
||||
),
|
||||
tag => crate::solana_core_unsupported_result(
|
||||
"unknown_compute_budget_instruction",
|
||||
"compute_budget_tag_unknown",
|
||||
format!(
|
||||
"unknown Compute Budget instruction tag {tag}; payload_sha256={}",
|
||||
crate::solana_core_payload_hash(input)
|
||||
),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
fn decode_u32_instruction(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
bytes: &[u8],
|
||||
accounts: serde_json::Value,
|
||||
entry_code: &str,
|
||||
field_name: &str,
|
||||
) -> crate::DecoderExecutionResult {
|
||||
if bytes.len() < 5 {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some(entry_code),
|
||||
"compute_budget_instruction_size_invalid",
|
||||
format!(
|
||||
"Compute Budget {entry_code} requires at least 5 bytes but received {}",
|
||||
bytes.len()
|
||||
),
|
||||
);
|
||||
}
|
||||
let value_result = crate::solana_core_read_u32_le(bytes, 1);
|
||||
let value = match value_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some(entry_code),
|
||||
"compute_budget_instruction_truncated",
|
||||
error.to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
let mut parameters = serde_json::Map::new();
|
||||
parameters.insert(field_name.to_string(), serde_json::json!(value));
|
||||
append_runtime_ignored_trailing_data(&mut parameters, bytes, 5);
|
||||
append_transaction_profile(input, &mut parameters);
|
||||
return crate::solana_core_decoded_result(
|
||||
input,
|
||||
crate::SOLANA_CORE_COMPUTE_BUDGET_SURFACE_CODE,
|
||||
entry_code,
|
||||
crate::EventFamily::Audit,
|
||||
false,
|
||||
accounts,
|
||||
serde_json::Value::Object(parameters),
|
||||
SOURCE_CURRENT,
|
||||
);
|
||||
}
|
||||
|
||||
fn decode_u64_instruction(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
bytes: &[u8],
|
||||
accounts: serde_json::Value,
|
||||
entry_code: &str,
|
||||
field_name: &str,
|
||||
) -> crate::DecoderExecutionResult {
|
||||
if bytes.len() < 9 {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some(entry_code),
|
||||
"compute_budget_instruction_size_invalid",
|
||||
format!(
|
||||
"Compute Budget {entry_code} requires at least 9 bytes but received {}",
|
||||
bytes.len()
|
||||
),
|
||||
);
|
||||
}
|
||||
let value_result = crate::solana_core_read_u64_le(bytes, 1);
|
||||
let value = match value_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some(entry_code),
|
||||
"compute_budget_instruction_truncated",
|
||||
error.to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
let mut parameters = serde_json::Map::new();
|
||||
parameters.insert(field_name.to_string(), serde_json::json!(value));
|
||||
append_runtime_ignored_trailing_data(&mut parameters, bytes, 9);
|
||||
append_transaction_profile(input, &mut parameters);
|
||||
return crate::solana_core_decoded_result(
|
||||
input,
|
||||
crate::SOLANA_CORE_COMPUTE_BUDGET_SURFACE_CODE,
|
||||
entry_code,
|
||||
crate::EventFamily::Audit,
|
||||
false,
|
||||
accounts,
|
||||
serde_json::Value::Object(parameters),
|
||||
SOURCE_CURRENT,
|
||||
);
|
||||
}
|
||||
|
||||
fn append_transaction_profile(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
parameters: &mut serde_json::Map<std::string::String, serde_json::Value>,
|
||||
) {
|
||||
parameters.insert("transactionProfile".to_string(), transaction_profile(input));
|
||||
}
|
||||
|
||||
fn transaction_profile(input: &crate::CoreInstructionReplayInput) -> serde_json::Value {
|
||||
let target_path = input.instruction_path.as_str();
|
||||
let mut instruction_rows = std::vec::Vec::new();
|
||||
let mut effective = serde_json::Map::new();
|
||||
let mut decoded_count = 0_u64;
|
||||
let mut unsupported_count = 0_u64;
|
||||
let mut failed_count = 0_u64;
|
||||
let mut trailing_count = 0_u64;
|
||||
let outer_rows = compute_budget_outer_rows(input);
|
||||
for row in outer_rows {
|
||||
if row.status == "decoded" {
|
||||
decoded_count = decoded_count.saturating_add(1);
|
||||
} else if row.status == "unsupported" {
|
||||
unsupported_count = unsupported_count.saturating_add(1);
|
||||
} else if row.status == "failed" {
|
||||
failed_count = failed_count.saturating_add(1);
|
||||
}
|
||||
if row.trailing_data_present {
|
||||
trailing_count = trailing_count.saturating_add(1);
|
||||
}
|
||||
apply_effective_value(&mut effective, &row);
|
||||
instruction_rows.push(row.to_json());
|
||||
}
|
||||
let emitter_path = profile_emitter_path(instruction_rows.as_slice());
|
||||
let current_is_emitter = emitter_path.as_deref() == std::option::Option::Some(target_path);
|
||||
return serde_json::json!({
|
||||
"profileVersion": 1,
|
||||
"profileSemantics": "transaction_compute_budget_last_write_wins",
|
||||
"currentInstructionIsProfileEmitter": current_is_emitter,
|
||||
"profileEmitterInstructionPath": emitter_path,
|
||||
"computeBudgetInstructionCount": instruction_rows.len(),
|
||||
"decodedInstructionCount": decoded_count,
|
||||
"unsupportedInstructionCount": unsupported_count,
|
||||
"failedInstructionCount": failed_count,
|
||||
"runtimeIgnoredTrailingDataInstructionCount": trailing_count,
|
||||
"effectiveValues": serde_json::Value::Object(effective),
|
||||
"instructions": instruction_rows,
|
||||
"transactionFinalRuntimeMetricsCaptured": false,
|
||||
"computeUnitsConsumedCaptured": false,
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct ComputeBudgetProfileRow {
|
||||
instruction_index: u64,
|
||||
instruction_path: std::string::String,
|
||||
entry_code: std::string::String,
|
||||
status: &'static str,
|
||||
parameters: serde_json::Value,
|
||||
payload_hash: serde_json::Value,
|
||||
trailing_data_present: bool,
|
||||
diagnostic: serde_json::Value,
|
||||
}
|
||||
|
||||
impl ComputeBudgetProfileRow {
|
||||
fn to_json(&self) -> serde_json::Value {
|
||||
return serde_json::json!({
|
||||
"instructionIndex": self.instruction_index,
|
||||
"instructionPath": self.instruction_path,
|
||||
"entryCode": self.entry_code,
|
||||
"status": self.status,
|
||||
"parameters": self.parameters,
|
||||
"payloadHash": self.payload_hash,
|
||||
"trailingDataPresent": self.trailing_data_present,
|
||||
"diagnostic": self.diagnostic,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_budget_outer_rows(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
) -> std::vec::Vec<ComputeBudgetProfileRow> {
|
||||
let mut rows = std::vec::Vec::new();
|
||||
let outer_values = match input.outer_instructions_json.as_array() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return rows,
|
||||
};
|
||||
for value in outer_values {
|
||||
if value.get("programId").and_then(serde_json::Value::as_str)
|
||||
!= std::option::Option::Some(kb_program_ids::COMPUTE_BUDGET_PROGRAM_ID)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let instruction_index =
|
||||
value.get("instructionIndex").and_then(serde_json::Value::as_u64).unwrap_or(0);
|
||||
let instruction_path = value
|
||||
.get("instructionPath")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let payload_hash = value.get("payloadHash").cloned().unwrap_or(serde_json::Value::Null);
|
||||
let payload_json = match value.get("payloadJson") {
|
||||
std::option::Option::Some(payload) if !payload.is_null() => payload,
|
||||
_ => {
|
||||
rows.push(profile_failure_row(
|
||||
instruction_index,
|
||||
instruction_path,
|
||||
payload_hash,
|
||||
"payload_missing",
|
||||
"outer Compute Budget instruction payload is not retained",
|
||||
));
|
||||
continue;
|
||||
},
|
||||
};
|
||||
let bytes_result = decode_payload_json_for_profile(payload_json);
|
||||
let bytes = match bytes_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
rows.push(profile_failure_row(
|
||||
instruction_index,
|
||||
instruction_path,
|
||||
payload_hash,
|
||||
"payload_invalid",
|
||||
error.as_str(),
|
||||
));
|
||||
continue;
|
||||
},
|
||||
};
|
||||
rows.push(profile_row_from_bytes(
|
||||
instruction_index,
|
||||
instruction_path,
|
||||
payload_hash,
|
||||
bytes.as_slice(),
|
||||
));
|
||||
}
|
||||
if rows.is_empty() && input.program_id == kb_program_ids::COMPUTE_BUDGET_PROGRAM_ID {
|
||||
let bytes_result = crate::solana_core_decode_instruction_data(input);
|
||||
let bytes = match bytes_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
rows.push(profile_failure_row(
|
||||
input.instruction_path.parse::<u64>().unwrap_or(0),
|
||||
input.instruction_path.clone(),
|
||||
serde_json::Value::String(crate::solana_core_payload_hash(input)),
|
||||
"payload_invalid",
|
||||
error.to_string().as_str(),
|
||||
));
|
||||
return rows;
|
||||
},
|
||||
};
|
||||
rows.push(profile_row_from_bytes(
|
||||
input.instruction_path.parse::<u64>().unwrap_or(0),
|
||||
input.instruction_path.clone(),
|
||||
serde_json::Value::String(crate::solana_core_payload_hash(input)),
|
||||
bytes.as_slice(),
|
||||
));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
fn profile_row_from_bytes(
|
||||
instruction_index: u64,
|
||||
instruction_path: std::string::String,
|
||||
payload_hash: serde_json::Value,
|
||||
bytes: &[u8],
|
||||
) -> ComputeBudgetProfileRow {
|
||||
let entry = entry_code(bytes).unwrap_or("unknown_compute_budget_instruction");
|
||||
let mut diagnostic = serde_json::Value::Null;
|
||||
let mut status = "decoded";
|
||||
let mut parameters = serde_json::Map::new();
|
||||
let mut trailing_data_present = false;
|
||||
match entry {
|
||||
"unused_reserved" => {},
|
||||
"request_units_deprecated" => {
|
||||
if bytes.len() == 9 {
|
||||
if let std::result::Result::Ok(value) = crate::solana_core_read_u32_le(bytes, 1) {
|
||||
parameters.insert("units".to_string(), serde_json::json!(value));
|
||||
}
|
||||
if let std::result::Result::Ok(value) = crate::solana_core_read_u32_le(bytes, 5) {
|
||||
parameters.insert("additionalFee".to_string(), serde_json::json!(value));
|
||||
}
|
||||
} else {
|
||||
status = "failed";
|
||||
diagnostic = serde_json::json!("historical request units payload size is invalid");
|
||||
}
|
||||
},
|
||||
"request_heap_frame" | "set_compute_unit_limit" | "set_loaded_accounts_data_size_limit" => {
|
||||
if bytes.len() >= 5 {
|
||||
if let std::result::Result::Ok(value) = crate::solana_core_read_u32_le(bytes, 1) {
|
||||
let field_name = match entry {
|
||||
"request_heap_frame" => "bytes",
|
||||
"set_compute_unit_limit" => "computeUnitLimit",
|
||||
_ => "bytes",
|
||||
};
|
||||
parameters.insert(field_name.to_string(), serde_json::json!(value));
|
||||
}
|
||||
trailing_data_present = bytes.len() > 5;
|
||||
} else {
|
||||
status = "failed";
|
||||
diagnostic = serde_json::json!("current u32 Compute Budget payload is truncated");
|
||||
}
|
||||
},
|
||||
"set_compute_unit_price" => {
|
||||
if bytes.len() >= 9 {
|
||||
if let std::result::Result::Ok(value) = crate::solana_core_read_u64_le(bytes, 1) {
|
||||
parameters.insert("microLamports".to_string(), serde_json::json!(value));
|
||||
}
|
||||
trailing_data_present = bytes.len() > 9;
|
||||
} else {
|
||||
status = "failed";
|
||||
diagnostic = serde_json::json!("current u64 Compute Budget payload is truncated");
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
status = "unsupported";
|
||||
diagnostic = serde_json::json!("unknown Compute Budget instruction tag");
|
||||
},
|
||||
}
|
||||
return ComputeBudgetProfileRow {
|
||||
instruction_index,
|
||||
instruction_path,
|
||||
entry_code: entry.to_string(),
|
||||
status,
|
||||
parameters: serde_json::Value::Object(parameters),
|
||||
payload_hash,
|
||||
trailing_data_present,
|
||||
diagnostic,
|
||||
};
|
||||
}
|
||||
|
||||
fn profile_failure_row(
|
||||
instruction_index: u64,
|
||||
instruction_path: std::string::String,
|
||||
payload_hash: serde_json::Value,
|
||||
code: &str,
|
||||
message: &str,
|
||||
) -> ComputeBudgetProfileRow {
|
||||
return ComputeBudgetProfileRow {
|
||||
instruction_index,
|
||||
instruction_path,
|
||||
entry_code: "malformed_compute_budget_instruction".to_string(),
|
||||
status: "failed",
|
||||
parameters: serde_json::Value::Null,
|
||||
payload_hash,
|
||||
trailing_data_present: false,
|
||||
diagnostic: serde_json::json!({"code": code, "message": message}),
|
||||
};
|
||||
}
|
||||
|
||||
fn decode_payload_json_for_profile(
|
||||
payload: &serde_json::Value,
|
||||
) -> std::result::Result<std::vec::Vec<u8>, std::string::String> {
|
||||
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::result::Result::Err("payload does not contain dataBase64".to_string());
|
||||
},
|
||||
};
|
||||
let maximum_encoded_length = crate::SOLANA_CORE_MAX_NATIVE_INSTRUCTION_PAYLOAD_BYTES
|
||||
.saturating_mul(4)
|
||||
.saturating_div(3)
|
||||
.saturating_add(4);
|
||||
if encoded.len() > maximum_encoded_length {
|
||||
return std::result::Result::Err(format!(
|
||||
"payload base64 exceeds {maximum_encoded_length} bytes"
|
||||
));
|
||||
}
|
||||
let decoded_result = base64::engine::general_purpose::STANDARD.decode(encoded.as_bytes());
|
||||
let decoded = match decoded_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(format!("payload is not valid base64: {error}"));
|
||||
},
|
||||
};
|
||||
if decoded.len() > crate::SOLANA_CORE_MAX_NATIVE_INSTRUCTION_PAYLOAD_BYTES {
|
||||
return std::result::Result::Err(format!(
|
||||
"payload exceeds {} decoded bytes",
|
||||
crate::SOLANA_CORE_MAX_NATIVE_INSTRUCTION_PAYLOAD_BYTES
|
||||
));
|
||||
}
|
||||
return std::result::Result::Ok(decoded);
|
||||
}
|
||||
|
||||
fn apply_effective_value(
|
||||
effective: &mut serde_json::Map<std::string::String, serde_json::Value>,
|
||||
row: &ComputeBudgetProfileRow,
|
||||
) {
|
||||
if row.status != "decoded" {
|
||||
return;
|
||||
}
|
||||
let value = match row.entry_code.as_str() {
|
||||
"request_units_deprecated" => serde_json::json!({
|
||||
"units": row.parameters.get("units").cloned().unwrap_or(serde_json::Value::Null),
|
||||
"additionalFee": row.parameters.get("additionalFee").cloned().unwrap_or(serde_json::Value::Null),
|
||||
}),
|
||||
"request_heap_frame" => {
|
||||
row.parameters.get("bytes").cloned().unwrap_or(serde_json::Value::Null)
|
||||
},
|
||||
"set_compute_unit_limit" => row
|
||||
.parameters
|
||||
.get("computeUnitLimit")
|
||||
.cloned()
|
||||
.unwrap_or(serde_json::Value::Null),
|
||||
"set_compute_unit_price" => {
|
||||
row.parameters.get("microLamports").cloned().unwrap_or(serde_json::Value::Null)
|
||||
},
|
||||
"set_loaded_accounts_data_size_limit" => {
|
||||
row.parameters.get("bytes").cloned().unwrap_or(serde_json::Value::Null)
|
||||
},
|
||||
_ => return,
|
||||
};
|
||||
effective.insert(
|
||||
row.entry_code.clone(),
|
||||
serde_json::json!({
|
||||
"value": value,
|
||||
"sourceInstructionIndex": row.instruction_index,
|
||||
"sourceInstructionPath": row.instruction_path,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
fn profile_emitter_path(
|
||||
instruction_rows: &[serde_json::Value],
|
||||
) -> std::option::Option<std::string::String> {
|
||||
for row in instruction_rows {
|
||||
if row.get("status").and_then(serde_json::Value::as_str)
|
||||
!= std::option::Option::Some("decoded")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if row.get("entryCode").and_then(serde_json::Value::as_str)
|
||||
== std::option::Option::Some("unused_reserved")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
return row
|
||||
.get("instructionPath")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(|value| return value.to_string());
|
||||
}
|
||||
return std::option::Option::None;
|
||||
}
|
||||
|
||||
fn append_runtime_ignored_trailing_data(
|
||||
parameters: &mut serde_json::Map<std::string::String, serde_json::Value>,
|
||||
bytes: &[u8],
|
||||
decoded_byte_length: usize,
|
||||
) {
|
||||
let trailing_data = match bytes.get(decoded_byte_length..) {
|
||||
std::option::Option::Some(value) if !value.is_empty() => value,
|
||||
_ => return,
|
||||
};
|
||||
parameters.insert("trailingDataByteLength".to_string(), serde_json::json!(trailing_data.len()));
|
||||
parameters.insert(
|
||||
"trailingDataSha256".to_string(),
|
||||
serde_json::json!(crate::solana_core_hash_bytes(trailing_data)),
|
||||
);
|
||||
parameters.insert(
|
||||
"trailingDataPrefixHex".to_string(),
|
||||
serde_json::json!(crate::solana_core_hexadecimal_prefix(
|
||||
trailing_data,
|
||||
trailing_data.len().min(16),
|
||||
)),
|
||||
);
|
||||
parameters.insert(
|
||||
"trailingDataSemantics".to_string(),
|
||||
serde_json::json!("ignored_by_runtime_borsh_unchecked"),
|
||||
);
|
||||
}
|
||||
|
||||
fn entry_code(bytes: &[u8]) -> std::option::Option<&'static str> {
|
||||
let tag = match bytes.first() {
|
||||
std::option::Option::Some(value) => *value,
|
||||
std::option::Option::None => return std::option::Option::None,
|
||||
};
|
||||
return match tag {
|
||||
0 if bytes.len() == 1 => std::option::Option::Some("unused_reserved"),
|
||||
0 if bytes.len() == 9 => std::option::Option::Some("request_units_deprecated"),
|
||||
1 => std::option::Option::Some("request_heap_frame"),
|
||||
2 => std::option::Option::Some("set_compute_unit_limit"),
|
||||
3 => std::option::Option::Some("set_compute_unit_price"),
|
||||
4 => std::option::Option::Some("set_loaded_accounts_data_size_limit"),
|
||||
_ => std::option::Option::None,
|
||||
};
|
||||
}
|
||||
|
||||
fn coverage_entry(
|
||||
entry_code: &str,
|
||||
discriminator_hex: &str,
|
||||
historical: bool,
|
||||
) -> crate::DecoderCoverageDeclaration {
|
||||
return crate::DecoderCoverageDeclaration {
|
||||
program_id: kb_program_ids::COMPUTE_BUDGET_PROGRAM_ID.to_string(),
|
||||
surface_code: std::option::Option::Some(
|
||||
crate::SOLANA_CORE_COMPUTE_BUDGET_SURFACE_CODE.to_string(),
|
||||
),
|
||||
entry_kind: crate::DecoderCoverageEntryKind::Instruction,
|
||||
entry_code: entry_code.to_string(),
|
||||
discriminator_hex: std::option::Option::Some(discriminator_hex.to_string()),
|
||||
historical,
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use base64::Engine; // rust-rules: trait-import
|
||||
|
||||
fn replay_input(bytes: &[u8], transaction_failed: bool) -> crate::CoreInstructionReplayInput {
|
||||
let encoded = base64::engine::general_purpose::STANDARD.encode(bytes);
|
||||
let result = crate::CoreInstructionReplayInput::new(
|
||||
"signature:0",
|
||||
"signature",
|
||||
42,
|
||||
"0",
|
||||
kb_program_ids::COMPUTE_BUDGET_PROGRAM_ID,
|
||||
transaction_failed,
|
||||
if transaction_failed {
|
||||
std::option::Option::Some(serde_json::json!({"InstructionError": [0, "Custom"]}))
|
||||
} else {
|
||||
std::option::Option::None
|
||||
},
|
||||
serde_json::json!([]),
|
||||
serde_json::json!([]),
|
||||
std::option::Option::Some(serde_json::json!({"dataBase64": encoded})),
|
||||
std::option::Option::Some("payload-hash".to_string()),
|
||||
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!("replay input failed: {error}"),
|
||||
};
|
||||
}
|
||||
|
||||
fn replay_input_with_outer(
|
||||
bytes: &[u8],
|
||||
instruction_path: &str,
|
||||
outer_instructions_json: serde_json::Value,
|
||||
) -> crate::CoreInstructionReplayInput {
|
||||
let encoded = base64::engine::general_purpose::STANDARD.encode(bytes);
|
||||
let result = crate::CoreInstructionReplayInput::new(
|
||||
"signature:0",
|
||||
"signature",
|
||||
42,
|
||||
instruction_path,
|
||||
kb_program_ids::COMPUTE_BUDGET_PROGRAM_ID,
|
||||
false,
|
||||
std::option::Option::None,
|
||||
serde_json::json!([]),
|
||||
serde_json::json!([]),
|
||||
std::option::Option::Some(serde_json::json!({"dataBase64": encoded})),
|
||||
std::option::Option::Some("payload-hash".to_string()),
|
||||
outer_instructions_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!("replay input failed: {error}"),
|
||||
};
|
||||
}
|
||||
|
||||
fn outer_instruction(
|
||||
index: u64,
|
||||
path: &str,
|
||||
program_id: &str,
|
||||
bytes: &[u8],
|
||||
) -> serde_json::Value {
|
||||
let encoded = base64::engine::general_purpose::STANDARD.encode(bytes);
|
||||
return serde_json::json!({
|
||||
"instructionIndex": index,
|
||||
"instructionPath": path,
|
||||
"programId": program_id,
|
||||
"payloadJson": {"dataBase64": encoded},
|
||||
"payloadHash": format!("hash-{path}"),
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transaction_profile_aggregates_outer_compute_budget_instructions_once() {
|
||||
let outer = serde_json::json!([
|
||||
outer_instruction(
|
||||
0,
|
||||
"0",
|
||||
kb_program_ids::COMPUTE_BUDGET_PROGRAM_ID,
|
||||
&[2, 64, 66, 15, 0],
|
||||
),
|
||||
outer_instruction(1, "1", kb_program_ids::SYSTEM_PROGRAM_ID, &[2, 1, 0, 0, 0],),
|
||||
outer_instruction(
|
||||
2,
|
||||
"2",
|
||||
kb_program_ids::COMPUTE_BUDGET_PROGRAM_ID,
|
||||
&[3, 7, 0, 0, 0, 0, 0, 0, 0],
|
||||
),
|
||||
outer_instruction(
|
||||
3,
|
||||
"3",
|
||||
kb_program_ids::COMPUTE_BUDGET_PROGRAM_ID,
|
||||
&[2, 128, 132, 30, 0],
|
||||
),
|
||||
]);
|
||||
let result = crate::solana_core_compute_budget_decode(&replay_input_with_outer(
|
||||
&[2, 64, 66, 15, 0],
|
||||
"0",
|
||||
outer,
|
||||
));
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
let profile = &result.observations[0].payload_json["parameters"]["transactionProfile"];
|
||||
assert_eq!(profile["currentInstructionIsProfileEmitter"], true);
|
||||
assert_eq!(profile["computeBudgetInstructionCount"], 3);
|
||||
assert_eq!(profile["effectiveValues"]["set_compute_unit_limit"]["value"], 2_000_000);
|
||||
assert_eq!(
|
||||
profile["effectiveValues"]["set_compute_unit_limit"]["sourceInstructionPath"],
|
||||
"3"
|
||||
);
|
||||
assert_eq!(profile["effectiveValues"]["set_compute_unit_price"]["value"], 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn official_current_encoder_matches_documented_layout() {
|
||||
let instruction =
|
||||
solana_compute_budget_interface::ComputeBudgetInstruction::SetComputeUnitPrice(7);
|
||||
let bytes_result = borsh::to_vec(&instruction);
|
||||
let bytes = match bytes_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
panic!("compute budget fixture serialization failed: {error}")
|
||||
},
|
||||
};
|
||||
assert_eq!(bytes, [3, 7, 0, 0, 0, 0, 0, 0, 0]);
|
||||
let result =
|
||||
crate::solana_core_compute_budget_decode(&replay_input(bytes.as_slice(), false));
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
assert_eq!(
|
||||
result.recognized_entry_code.as_deref(),
|
||||
std::option::Option::Some("set_compute_unit_price")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_current_variants_decode_with_exact_numeric_values() {
|
||||
let fixtures = [
|
||||
(vec![1, 0, 0, 4, 0], "request_heap_frame"),
|
||||
(vec![2, 255, 255, 255, 255], "set_compute_unit_limit"),
|
||||
(vec![3, 255, 255, 255, 255, 255, 255, 255, 255], "set_compute_unit_price"),
|
||||
(vec![4, 0, 0, 0, 1], "set_loaded_accounts_data_size_limit"),
|
||||
];
|
||||
for (bytes, expected) in fixtures {
|
||||
let result =
|
||||
crate::solana_core_compute_budget_decode(&replay_input(bytes.as_slice(), false));
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
assert_eq!(
|
||||
result.recognized_entry_code.as_deref(),
|
||||
std::option::Option::Some(expected)
|
||||
);
|
||||
assert_eq!(result.observations.len(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn historical_request_units_is_decoded() {
|
||||
let result = crate::solana_core_compute_budget_decode(&replay_input(
|
||||
&[0, 64, 13, 3, 0, 7, 0, 0, 0],
|
||||
false,
|
||||
));
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
assert_eq!(
|
||||
result.recognized_entry_code.as_deref(),
|
||||
std::option::Option::Some("request_units_deprecated")
|
||||
);
|
||||
assert_eq!(result.observations[0].payload_json["parameters"]["units"], 200_000);
|
||||
assert_eq!(result.observations[0].payload_json["parameters"]["additionalFee"], 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reserved_unused_tag_is_ignored_without_false_event() {
|
||||
let result = crate::solana_core_compute_budget_decode(&replay_input(&[0], false));
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Ignored);
|
||||
assert!(result.observations.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncated_and_unknown_payloads_are_distinguished() {
|
||||
let truncated = crate::solana_core_compute_budget_decode(&replay_input(&[2, 1, 2], false));
|
||||
assert_eq!(truncated.status, crate::DecoderOutcomeStatus::Failed);
|
||||
let unknown =
|
||||
crate::solana_core_compute_budget_decode(&replay_input(&[255, 1, 2, 3], false));
|
||||
assert_eq!(unknown.status, crate::DecoderOutcomeStatus::Unsupported);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn current_variants_accept_and_preserve_runtime_ignored_trailing_bytes() {
|
||||
let result = crate::solana_core_compute_budget_decode(&replay_input(
|
||||
&[2, 64, 13, 3, 0, 1, 2, 3, 4, 5, 6, 7],
|
||||
true,
|
||||
));
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
assert_eq!(
|
||||
result.recognized_entry_code.as_deref(),
|
||||
std::option::Option::Some("set_compute_unit_limit")
|
||||
);
|
||||
assert_eq!(result.observations[0].payload_json["parameters"]["computeUnitLimit"], 200_000);
|
||||
assert_eq!(result.observations[0].payload_json["parameters"]["trailingDataByteLength"], 7);
|
||||
assert_eq!(
|
||||
result.observations[0].payload_json["parameters"]["trailingDataPrefixHex"],
|
||||
"01020304050607"
|
||||
);
|
||||
assert_eq!(
|
||||
result.observations[0].payload_json["parameters"]["trailingDataSemantics"],
|
||||
"ignored_by_runtime_borsh_unchecked"
|
||||
);
|
||||
assert!(result.observations[0].transaction_failed);
|
||||
assert!(!result.observations[0].observation_committed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_transaction_event_is_not_committed() {
|
||||
let result =
|
||||
crate::solana_core_compute_budget_decode(&replay_input(&[2, 64, 13, 3, 0], true));
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
assert!(result.observations[0].transaction_failed);
|
||||
assert!(!result.observations[0].observation_committed);
|
||||
assert_eq!(result.observations[0].payload_json["transactionSucceeded"], false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coverage_declares_current_and_historical_entries() {
|
||||
let coverage = crate::solana_core_compute_budget_coverage();
|
||||
assert_eq!(coverage.len(), 6);
|
||||
assert!(coverage.iter().any(|entry| {
|
||||
return entry.entry_code == "request_units_deprecated" && entry.historical;
|
||||
}));
|
||||
}
|
||||
}
|
||||
491
kb-lib/src/decoder/solana/core/config.rs
Normal file
491
kb-lib/src/decoder/solana/core/config.rs
Normal file
@@ -0,0 +1,491 @@
|
||||
// file: kb-lib/src/decoder/solana/core/config.rs
|
||||
// version: 4
|
||||
|
||||
//! Generic bounded Config Program store decoding without inventing payload semantics.
|
||||
|
||||
const SOURCE: &str =
|
||||
"solana-config-interface@2.0.0 plus solana-config-program@2.2.20 processor contract";
|
||||
const MAX_CONFIG_KEYS: usize = 128;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
struct ParsedConfigPayload {
|
||||
keys: std::vec::Vec<(std::string::String, bool)>,
|
||||
data_offset: usize,
|
||||
}
|
||||
|
||||
/// Returns declared Config Program instruction coverage.
|
||||
pub(crate) fn config_coverage() -> std::vec::Vec<crate::DecoderCoverageDeclaration> {
|
||||
return vec![crate::DecoderCoverageDeclaration {
|
||||
program_id: kb_program_ids::CONFIG_PROGRAM_ID.to_string(),
|
||||
surface_code: std::option::Option::Some(crate::SOLANA_CORE_CONFIG_SURFACE_CODE.to_string()),
|
||||
entry_kind: crate::DecoderCoverageEntryKind::Instruction,
|
||||
entry_code: "store".to_string(),
|
||||
discriminator_hex: std::option::Option::None,
|
||||
historical: false,
|
||||
}];
|
||||
}
|
||||
|
||||
/// Recognizes one generic Config Program store instruction.
|
||||
pub(crate) fn config_recognize(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
priority: u16,
|
||||
) -> crate::DecoderRecognition {
|
||||
let bytes_result = crate::solana_core_decode_instruction_data(input);
|
||||
let bytes = match bytes_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_error) => {
|
||||
return crate::DecoderRecognition::compatible(
|
||||
false,
|
||||
priority,
|
||||
std::option::Option::Some(crate::SOLANA_CORE_CONFIG_SURFACE_CODE.to_string()),
|
||||
std::option::Option::Some("malformed_config_store".to_string()),
|
||||
std::option::Option::None,
|
||||
);
|
||||
},
|
||||
};
|
||||
let exact = parse_payload(bytes.as_slice()).is_ok();
|
||||
return crate::DecoderRecognition::compatible(
|
||||
exact,
|
||||
priority,
|
||||
std::option::Option::Some(crate::SOLANA_CORE_CONFIG_SURFACE_CODE.to_string()),
|
||||
std::option::Option::Some(if exact {
|
||||
"store".to_string()
|
||||
} else {
|
||||
"malformed_config_store".to_string()
|
||||
}),
|
||||
crate::solana_core_hexadecimal_prefix(bytes.as_slice(), bytes.len().min(3)),
|
||||
);
|
||||
}
|
||||
|
||||
/// Decodes one generic Config Program store instruction.
|
||||
pub(crate) fn config_decode(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
) -> crate::DecoderExecutionResult {
|
||||
let bytes_result = crate::solana_core_decode_instruction_data(input);
|
||||
let bytes = match bytes_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some("malformed_config_store"),
|
||||
"config_payload_invalid",
|
||||
error.to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
let parsed_result = parse_payload(bytes.as_slice());
|
||||
let parsed = match parsed_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some("store"),
|
||||
"config_keys_invalid",
|
||||
error.to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
let accounts_result = resolve_and_validate_accounts(input, &parsed);
|
||||
let accounts = match accounts_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some("store"),
|
||||
"config_accounts_invalid",
|
||||
error.to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
let data = match bytes.get(parsed.data_offset..) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some("store"),
|
||||
"config_data_offset_invalid",
|
||||
"Config data offset exceeds retained payload",
|
||||
);
|
||||
},
|
||||
};
|
||||
let keys_json = parsed
|
||||
.keys
|
||||
.iter()
|
||||
.map(|(pubkey, signer)| {
|
||||
return serde_json::json!({"pubkey": pubkey, "signer": signer});
|
||||
})
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
let signer_count = parsed.keys.iter().filter(|(_, signer)| return *signer).count();
|
||||
return crate::solana_core_decoded_result(
|
||||
input,
|
||||
crate::SOLANA_CORE_CONFIG_SURFACE_CODE,
|
||||
"store",
|
||||
crate::EventFamily::Admin,
|
||||
false,
|
||||
accounts,
|
||||
serde_json::json!({
|
||||
"configuredKeys": keys_json,
|
||||
"configuredKeyCount": parsed.keys.len(),
|
||||
"configuredSignerCount": signer_count,
|
||||
"configDataByteLength": data.len(),
|
||||
"configDataSha256": crate::solana_core_hash_bytes(data),
|
||||
"configDataPrefixHex": crate::solana_core_hexadecimal_prefix(data, data.len().min(16)),
|
||||
"configDataSemantics": "opaque_program_specific",
|
||||
}),
|
||||
SOURCE,
|
||||
);
|
||||
}
|
||||
|
||||
fn parse_payload(bytes: &[u8]) -> kb_core::Result<ParsedConfigPayload> {
|
||||
let length_result = read_compact_u16(bytes);
|
||||
let (key_count, mut offset) = match length_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let key_count_usize = usize::from(key_count);
|
||||
if key_count_usize > MAX_CONFIG_KEYS {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||
"Config key count {key_count_usize} exceeds bounded maximum {}",
|
||||
MAX_CONFIG_KEYS
|
||||
)));
|
||||
}
|
||||
let required = offset.saturating_add(key_count_usize.saturating_mul(33));
|
||||
if required > bytes.len() {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||
"Config key vector requires {required} bytes but payload contains {}",
|
||||
bytes.len()
|
||||
)));
|
||||
}
|
||||
let mut keys = std::vec::Vec::with_capacity(key_count_usize);
|
||||
for index in 0..key_count_usize {
|
||||
let pubkey_end = offset.saturating_add(32);
|
||||
let pubkey_bytes = match bytes.get(offset..pubkey_end) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||
"Config key {index} pubkey is truncated"
|
||||
)));
|
||||
},
|
||||
};
|
||||
let signer_byte = match bytes.get(pubkey_end) {
|
||||
std::option::Option::Some(value) => *value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||
"Config key {index} signer flag is truncated"
|
||||
)));
|
||||
},
|
||||
};
|
||||
let signer = match signer_byte {
|
||||
0 => false,
|
||||
1 => true,
|
||||
value => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||
"Config key {index} signer flag {value} is not a canonical bool"
|
||||
)));
|
||||
},
|
||||
};
|
||||
let pubkey = bs58::encode(pubkey_bytes).into_string();
|
||||
if keys.iter().any(|(existing_pubkey, existing_signer)| {
|
||||
return existing_pubkey == &pubkey && *existing_signer == signer;
|
||||
}) {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||
"Config key {index} duplicates an earlier key/signature pair"
|
||||
)));
|
||||
}
|
||||
keys.push((pubkey, signer));
|
||||
offset = pubkey_end.saturating_add(1);
|
||||
}
|
||||
return std::result::Result::Ok(ParsedConfigPayload { keys, data_offset: offset });
|
||||
}
|
||||
|
||||
fn read_compact_u16(bytes: &[u8]) -> kb_core::Result<(u16, usize)> {
|
||||
let first = match bytes.first() {
|
||||
std::option::Option::Some(value) => *value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(
|
||||
"Config compact-u16 key count is missing",
|
||||
));
|
||||
},
|
||||
};
|
||||
let mut value = u16::from(first & 0x7f);
|
||||
if first & 0x80 == 0 {
|
||||
return std::result::Result::Ok((value, 1));
|
||||
}
|
||||
let second = match bytes.get(1) {
|
||||
std::option::Option::Some(value) => *value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(
|
||||
"Config compact-u16 key count is truncated after one byte",
|
||||
));
|
||||
},
|
||||
};
|
||||
value |= u16::from(second & 0x7f) << 7;
|
||||
if second & 0x80 == 0 {
|
||||
if value < 128 {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(
|
||||
"Config compact-u16 key count uses a non-canonical two-byte encoding",
|
||||
));
|
||||
}
|
||||
return std::result::Result::Ok((value, 2));
|
||||
}
|
||||
let third = match bytes.get(2) {
|
||||
std::option::Option::Some(value) => *value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(
|
||||
"Config compact-u16 key count is truncated after two bytes",
|
||||
));
|
||||
},
|
||||
};
|
||||
if third & 0xfc != 0 {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(
|
||||
"Config compact-u16 key count exceeds u16 or has a continuation bit",
|
||||
));
|
||||
}
|
||||
value |= u16::from(third) << 14;
|
||||
if value < 16_384 {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(
|
||||
"Config compact-u16 key count uses a non-canonical three-byte encoding",
|
||||
));
|
||||
}
|
||||
return std::result::Result::Ok((value, 3));
|
||||
}
|
||||
|
||||
fn resolve_and_validate_accounts(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
parsed: &ParsedConfigPayload,
|
||||
) -> kb_core::Result<serde_json::Value> {
|
||||
let instruction_accounts = match input.instruction_accounts_json.as_array() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(
|
||||
"Config instruction accounts must be an array",
|
||||
));
|
||||
},
|
||||
};
|
||||
let config_key = match instruction_accounts
|
||||
.first()
|
||||
.and_then(|value| return value.get("accountKey"))
|
||||
.and_then(serde_json::Value::as_str)
|
||||
{
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(
|
||||
"Config instruction requires a config account at position zero",
|
||||
));
|
||||
},
|
||||
};
|
||||
let required_signers = parsed
|
||||
.keys
|
||||
.iter()
|
||||
.filter(|(pubkey, signer)| return *signer && pubkey != config_key)
|
||||
.map(|(pubkey, _signer)| return pubkey.as_str())
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
let mut roles = std::vec::Vec::with_capacity(required_signers.len().saturating_add(1));
|
||||
roles.push(crate::SolanaCoreAccountRole::optional_signer("config_account", true));
|
||||
for _signer in &required_signers {
|
||||
roles.push(crate::SolanaCoreAccountRole::new("configured_signer", true, true));
|
||||
}
|
||||
let minimum_count = required_signers.len().saturating_add(1);
|
||||
let accounts = match crate::solana_core_resolve_accounts(
|
||||
input,
|
||||
roles.as_slice(),
|
||||
minimum_count,
|
||||
std::option::Option::None,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let resolved = match accounts.as_array() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(
|
||||
"resolved Config accounts must be an array",
|
||||
));
|
||||
},
|
||||
};
|
||||
for (index, expected) in required_signers.iter().enumerate() {
|
||||
let position = index.saturating_add(1);
|
||||
let actual = resolved
|
||||
.get(position)
|
||||
.and_then(|value| return value.get("accountKey"))
|
||||
.and_then(serde_json::Value::as_str);
|
||||
if actual != std::option::Option::Some(*expected) {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||
"Config signer account {position} must match encoded key {expected}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(accounts);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use base64::Engine; // rust-rules: trait-import
|
||||
|
||||
fn encode_payload(keys: &[([u8; 32], bool)], data: &[u8]) -> std::vec::Vec<u8> {
|
||||
let mut output = std::vec::Vec::new();
|
||||
let count = keys.len();
|
||||
if count < 128 {
|
||||
output.push(count as u8);
|
||||
} else {
|
||||
panic!("test fixture key count exceeds one-byte compact encoding");
|
||||
}
|
||||
for (pubkey, signer) in keys {
|
||||
output.extend_from_slice(pubkey);
|
||||
output.push(u8::from(*signer));
|
||||
}
|
||||
output.extend_from_slice(data);
|
||||
return output;
|
||||
}
|
||||
|
||||
fn replay_input(
|
||||
bytes: &[u8],
|
||||
extra_accounts: &[std::string::String],
|
||||
transaction_failed: bool,
|
||||
) -> crate::CoreInstructionReplayInput {
|
||||
let mut keys = vec!["ConfigAccount1111111111111111111111111111".to_string()];
|
||||
keys.extend_from_slice(extra_accounts);
|
||||
let account_keys = keys
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, key)| {
|
||||
return serde_json::json!({
|
||||
"accountIndex": index,
|
||||
"accountKey": key,
|
||||
"source": "static",
|
||||
"writable": true,
|
||||
"signer": true,
|
||||
"executable": false,
|
||||
});
|
||||
})
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
let instruction_accounts = keys
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, key)| {
|
||||
return serde_json::json!({"accountIndex": index, "accountKey": key});
|
||||
})
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
let result = crate::CoreInstructionReplayInput::new(
|
||||
"signature:0",
|
||||
"signature",
|
||||
42,
|
||||
"0",
|
||||
kb_program_ids::CONFIG_PROGRAM_ID,
|
||||
transaction_failed,
|
||||
if transaction_failed {
|
||||
std::option::Option::Some(serde_json::json!({"InstructionError": [0, "Custom"]}))
|
||||
} else {
|
||||
std::option::Option::None
|
||||
},
|
||||
serde_json::Value::Array(account_keys),
|
||||
serde_json::Value::Array(instruction_accounts),
|
||||
std::option::Option::Some(serde_json::json!({
|
||||
"dataBase64": base64::engine::general_purpose::STANDARD.encode(bytes),
|
||||
})),
|
||||
std::option::Option::Some("payload-hash".to_string()),
|
||||
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!("Config replay input failed: {error}"),
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_generic_store_is_recognized_exactly() {
|
||||
let payload = encode_payload(&[], &[1, 2, 3]);
|
||||
let input = replay_input(payload.as_slice(), &[], false);
|
||||
let recognition = crate::solana_core_config_recognize(&input, 100);
|
||||
assert!(recognition.compatible);
|
||||
assert!(recognition.exact);
|
||||
assert_eq!(recognition.entry_code.as_deref(), std::option::Option::Some("store"));
|
||||
let malformed = crate::solana_core_config_recognize(&replay_input(&[], &[], false), 100);
|
||||
assert!(malformed.compatible);
|
||||
assert!(!malformed.exact);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_key_set_and_opaque_data_decode_without_invented_semantics() {
|
||||
let payload = encode_payload(&[], &[1, 2, 3, 4]);
|
||||
let result =
|
||||
crate::solana_core_config_decode(&replay_input(payload.as_slice(), &[], false));
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
assert_eq!(result.observations[0].payload_json["parameters"]["configuredKeyCount"], 0);
|
||||
assert_eq!(result.observations[0].payload_json["parameters"]["configDataByteLength"], 4);
|
||||
assert_eq!(
|
||||
result.observations[0].payload_json["parameters"]["configDataSemantics"],
|
||||
"opaque_program_specific"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encoded_signer_keys_are_resolved_in_official_account_order() {
|
||||
let signer_bytes = [7_u8; 32];
|
||||
let signer = bs58::encode(signer_bytes).into_string();
|
||||
let payload = encode_payload(&[([3_u8; 32], false), (signer_bytes, true)], &[9]);
|
||||
let result =
|
||||
crate::solana_core_config_decode(&replay_input(payload.as_slice(), &[signer], false));
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
assert_eq!(result.observations[0].payload_json["parameters"]["configuredSignerCount"], 1);
|
||||
assert_eq!(result.observations[0].payload_json["accounts"][1]["role"], "configured_signer");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_compact_length_keys_and_bool_fail_safely() {
|
||||
for payload in [
|
||||
vec![],
|
||||
vec![0x80],
|
||||
vec![1, 2, 3],
|
||||
{
|
||||
let mut value = vec![1];
|
||||
value.extend_from_slice(&[0_u8; 32]);
|
||||
value.push(2);
|
||||
value
|
||||
},
|
||||
vec![0x80, 0],
|
||||
{
|
||||
let mut value = vec![2];
|
||||
value.extend_from_slice(&[4_u8; 32]);
|
||||
value.push(1);
|
||||
value.extend_from_slice(&[4_u8; 32]);
|
||||
value.push(1);
|
||||
value
|
||||
},
|
||||
] {
|
||||
let result =
|
||||
crate::solana_core_config_decode(&replay_input(payload.as_slice(), &[], false));
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Failed);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_or_mismatched_signer_accounts_fail() {
|
||||
let signer_bytes = [8_u8; 32];
|
||||
let payload = encode_payload(&[(signer_bytes, true)], &[]);
|
||||
let missing =
|
||||
crate::solana_core_config_decode(&replay_input(payload.as_slice(), &[], false));
|
||||
assert_eq!(missing.status, crate::DecoderOutcomeStatus::Failed);
|
||||
let mismatch = crate::solana_core_config_decode(&replay_input(
|
||||
payload.as_slice(),
|
||||
&[bs58::encode([9_u8; 32]).into_string()],
|
||||
false,
|
||||
));
|
||||
assert_eq!(mismatch.status, crate::DecoderOutcomeStatus::Failed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_config_store_is_decoded_as_uncommitted_intent() {
|
||||
let payload = encode_payload(&[], &[5]);
|
||||
let result = crate::solana_core_config_decode(&replay_input(payload.as_slice(), &[], true));
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
assert!(!result.observations[0].observation_committed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coverage_declares_one_generic_store_surface() {
|
||||
let coverage = crate::solana_core_config_coverage();
|
||||
assert_eq!(coverage.len(), 1);
|
||||
assert_eq!(coverage[0].entry_code, "store");
|
||||
assert!(coverage[0].discriminator_hex.is_none());
|
||||
}
|
||||
}
|
||||
66
kb-lib/src/decoder/solana/core/constants.rs
Normal file
66
kb-lib/src/decoder/solana/core/constants.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
// file: kb-lib/src/decoder/solana/core/constants.rs
|
||||
// version: 13
|
||||
|
||||
//! Local constants for the Solana Core decoder component.
|
||||
|
||||
/// Stable protocol code shared by native Solana events.
|
||||
pub(crate) const PROTOCOL_CODE: &str = "solana_native";
|
||||
/// Stable System Program surface code.
|
||||
pub(crate) const SYSTEM_SURFACE_CODE: &str = "solana_native_system";
|
||||
/// Stable deprecated immutable BPF Loader surface code.
|
||||
pub(crate) const BPF_LOADER_DEPRECATED_SURFACE_CODE: &str = "solana_native_bpf_loader_deprecated";
|
||||
/// Stable immutable BPF Loader v2 surface code.
|
||||
pub(crate) const BPF_LOADER_SURFACE_CODE: &str = "solana_native_bpf_loader";
|
||||
/// Stable upgradeable BPF Loader surface code.
|
||||
pub(crate) const BPF_LOADER_UPGRADEABLE_SURFACE_CODE: &str = "solana_native_bpf_loader_upgradeable";
|
||||
/// Stable Loader v4 surface code.
|
||||
pub(crate) const LOADER_V4_SURFACE_CODE: &str = "solana_native_loader_v4";
|
||||
/// Stable Native Loader surface code.
|
||||
pub(crate) const NATIVE_LOADER_SURFACE_CODE: &str = "solana_native_loader";
|
||||
/// Stable Compute Budget surface code.
|
||||
pub(crate) const COMPUTE_BUDGET_SURFACE_CODE: &str = "solana_native_compute_budget";
|
||||
/// Stable Address Lookup Table surface code.
|
||||
pub(crate) const ADDRESS_LOOKUP_TABLE_SURFACE_CODE: &str = "solana_native_address_lookup_table";
|
||||
/// Stable Vote Program surface code.
|
||||
pub(crate) const VOTE_SURFACE_CODE: &str = "solana_native_vote";
|
||||
/// Stable Stake Program surface code.
|
||||
pub(crate) const STAKE_SURFACE_CODE: &str = "solana_native_stake";
|
||||
/// Stable Config Program surface code.
|
||||
pub(crate) const CONFIG_SURFACE_CODE: &str = "solana_native_config";
|
||||
/// Stable Feature Gate surface code.
|
||||
pub(crate) const FEATURE_SURFACE_CODE: &str = "solana_native_feature";
|
||||
/// Stable Ed25519 signature precompile surface code.
|
||||
pub(crate) const ED25519_SURFACE_CODE: &str = "solana_native_ed25519";
|
||||
/// Stable secp256k1 signature precompile surface code.
|
||||
pub(crate) const SECP256K1_SURFACE_CODE: &str = "solana_native_secp256k1";
|
||||
/// Stable secp256r1 signature precompile surface code.
|
||||
pub(crate) const SECP256R1_SURFACE_CODE: &str = "solana_native_secp256r1";
|
||||
/// Stable native ZK ElGamal Proof surface code.
|
||||
pub(crate) const ZK_ELGAMAL_PROOF_SURFACE_CODE: &str = "solana_native_zk_elgamal_proof";
|
||||
/// Stable historical ZK Token Proof surface code.
|
||||
pub(crate) const ZK_TOKEN_PROOF_SURFACE_CODE: &str = "solana_native_zk_token_proof";
|
||||
/// Byte length shared by compact Ed25519, secp256k1 and secp256r1 signatures.
|
||||
pub(crate) const SIGNATURE_BYTES: usize = 64;
|
||||
/// Byte length of an Ed25519 public key.
|
||||
pub(crate) const ED25519_PUBLIC_KEY_BYTES: usize = 32;
|
||||
/// Byte length of a compressed secp256r1 public key.
|
||||
pub(crate) const SECP256R1_PUBLIC_KEY_BYTES: usize = 33;
|
||||
/// Byte length of a secp256k1 Ethereum address.
|
||||
pub(crate) const SECP256K1_ETHEREUM_ADDRESS_BYTES: usize = 20;
|
||||
/// Byte length of one Ed25519 or secp256r1 offsets entry.
|
||||
pub(crate) const U16_OFFSETS_BYTES: usize = 14;
|
||||
/// Byte length of one secp256k1 offsets entry.
|
||||
pub(crate) const U8_OFFSETS_BYTES: usize = 11;
|
||||
/// Stable Slashing Program decoder surface code.
|
||||
pub(crate) const SLASHING_SURFACE_CODE: &str = "solana_native_slashing";
|
||||
/// Runtime maximum number of secp256r1 signatures in one precompile instruction.
|
||||
pub(crate) const SECP256R1_MAX_SIGNATURES: usize = 8;
|
||||
/// Maximum number of component bytes retained as a hexadecimal event prefix.
|
||||
pub(crate) const PRECOMPILE_COMPONENT_PREFIX_BYTES: usize = 16;
|
||||
/// Maximum retained native instruction payload accepted by the first decoder phase.
|
||||
pub(crate) const MAX_NATIVE_INSTRUCTION_PAYLOAD_BYTES: usize = 4_096;
|
||||
/// Current native event payload contract version.
|
||||
pub(crate) const NATIVE_EVENT_VERSION: u32 = 1;
|
||||
|
||||
/// Canonical tracing target for the Solana Core decoder component.
|
||||
pub(crate) const TRACING_TARGET: &str = "kb-lib.decoder.solana.core";
|
||||
605
kb-lib/src/decoder/solana/core/decoder.rs
Normal file
605
kb-lib/src/decoder/solana/core/decoder.rs
Normal file
@@ -0,0 +1,605 @@
|
||||
// file: kb-lib/src/decoder/solana/core/decoder.rs
|
||||
// version: 24
|
||||
|
||||
//! Runtime-native Solana program classifier for the common decode pipeline.
|
||||
|
||||
const NATIVE_SURFACES: &[crate::DecoderSurface] = &[
|
||||
crate::DecoderSurface {
|
||||
program_id: kb_program_ids::ADDRESS_LOOKUP_TABLE_PROGRAM_ID,
|
||||
surface_code: "solana_native_address_lookup_table",
|
||||
priority: 100,
|
||||
},
|
||||
crate::DecoderSurface {
|
||||
program_id: kb_program_ids::BPF_LOADER_DEPRECATED_PROGRAM_ID,
|
||||
surface_code: "solana_native_bpf_loader_deprecated",
|
||||
priority: 100,
|
||||
},
|
||||
crate::DecoderSurface {
|
||||
program_id: kb_program_ids::BPF_LOADER_PROGRAM_ID,
|
||||
surface_code: "solana_native_bpf_loader",
|
||||
priority: 100,
|
||||
},
|
||||
crate::DecoderSurface {
|
||||
program_id: kb_program_ids::BPF_LOADER_UPGRADEABLE_PROGRAM_ID,
|
||||
surface_code: "solana_native_bpf_loader_upgradeable",
|
||||
priority: 100,
|
||||
},
|
||||
crate::DecoderSurface {
|
||||
program_id: kb_program_ids::COMPUTE_BUDGET_PROGRAM_ID,
|
||||
surface_code: "solana_native_compute_budget",
|
||||
priority: 100,
|
||||
},
|
||||
crate::DecoderSurface {
|
||||
program_id: kb_program_ids::CONFIG_PROGRAM_ID,
|
||||
surface_code: "solana_native_config",
|
||||
priority: 100,
|
||||
},
|
||||
crate::DecoderSurface {
|
||||
program_id: kb_program_ids::ED25519_PROGRAM_ID,
|
||||
surface_code: "solana_native_ed25519",
|
||||
priority: 100,
|
||||
},
|
||||
crate::DecoderSurface {
|
||||
program_id: kb_program_ids::FEATURE_PROGRAM_ID,
|
||||
surface_code: "solana_native_feature",
|
||||
priority: 100,
|
||||
},
|
||||
crate::DecoderSurface {
|
||||
program_id: kb_program_ids::LOADER_V4_PROGRAM_ID,
|
||||
surface_code: "solana_native_loader_v4",
|
||||
priority: 100,
|
||||
},
|
||||
crate::DecoderSurface {
|
||||
program_id: kb_program_ids::NATIVE_LOADER_PROGRAM_ID,
|
||||
surface_code: "solana_native_loader",
|
||||
priority: 100,
|
||||
},
|
||||
crate::DecoderSurface {
|
||||
program_id: kb_program_ids::SECP256K1_PROGRAM_ID,
|
||||
surface_code: "solana_native_secp256k1",
|
||||
priority: 100,
|
||||
},
|
||||
crate::DecoderSurface {
|
||||
program_id: kb_program_ids::SECP256R1_PROGRAM_ID,
|
||||
surface_code: "solana_native_secp256r1",
|
||||
priority: 100,
|
||||
},
|
||||
crate::DecoderSurface {
|
||||
program_id: kb_program_ids::SLASHING_PROGRAM_ID,
|
||||
surface_code: "solana_native_slashing",
|
||||
priority: 100,
|
||||
},
|
||||
crate::DecoderSurface {
|
||||
program_id: kb_program_ids::STAKE_PROGRAM_ID,
|
||||
surface_code: "solana_native_stake",
|
||||
priority: 100,
|
||||
},
|
||||
crate::DecoderSurface {
|
||||
program_id: kb_program_ids::SYSTEM_PROGRAM_ID,
|
||||
surface_code: "solana_native_system",
|
||||
priority: 100,
|
||||
},
|
||||
crate::DecoderSurface {
|
||||
program_id: kb_program_ids::VOTE_PROGRAM_ID,
|
||||
surface_code: "solana_native_vote",
|
||||
priority: 100,
|
||||
},
|
||||
crate::DecoderSurface {
|
||||
program_id: kb_program_ids::ZK_ELGAMAL_PROOF_PROGRAM_ID,
|
||||
surface_code: "solana_native_zk_elgamal_proof",
|
||||
priority: 100,
|
||||
},
|
||||
crate::DecoderSurface {
|
||||
program_id: kb_program_ids::ZK_TOKEN_PROOF_PROGRAM_ID,
|
||||
surface_code: "solana_native_zk_token_proof",
|
||||
priority: 100,
|
||||
},
|
||||
];
|
||||
|
||||
const LEGACY_PROGRAM_IDS: &[&str] = &[
|
||||
kb_program_ids::ADDRESS_LOOKUP_TABLE_PROGRAM_ID,
|
||||
kb_program_ids::BPF_LOADER_DEPRECATED_PROGRAM_ID,
|
||||
kb_program_ids::BPF_LOADER_PROGRAM_ID,
|
||||
kb_program_ids::BPF_LOADER_UPGRADEABLE_PROGRAM_ID,
|
||||
kb_program_ids::COMPUTE_BUDGET_PROGRAM_ID,
|
||||
kb_program_ids::CONFIG_PROGRAM_ID,
|
||||
kb_program_ids::ED25519_PROGRAM_ID,
|
||||
kb_program_ids::FEATURE_PROGRAM_ID,
|
||||
kb_program_ids::LOADER_V4_PROGRAM_ID,
|
||||
kb_program_ids::NATIVE_LOADER_PROGRAM_ID,
|
||||
kb_program_ids::SECP256K1_PROGRAM_ID,
|
||||
kb_program_ids::SECP256R1_PROGRAM_ID,
|
||||
kb_program_ids::SLASHING_PROGRAM_ID,
|
||||
kb_program_ids::STAKE_PROGRAM_ID,
|
||||
kb_program_ids::SYSTEM_PROGRAM_ID,
|
||||
kb_program_ids::VOTE_PROGRAM_ID,
|
||||
kb_program_ids::ZK_ELGAMAL_PROOF_PROGRAM_ID,
|
||||
kb_program_ids::ZK_TOKEN_PROOF_PROGRAM_ID,
|
||||
];
|
||||
|
||||
/// Runtime-native Solana decoder with phased maximal instruction coverage.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct SolanaCoreDecoder;
|
||||
|
||||
impl crate::ProtocolDecoder for crate::SolanaCoreDecoder {
|
||||
fn decoder_name(&self) -> &'static str {
|
||||
return "kb_decoder_solana_core";
|
||||
}
|
||||
|
||||
fn decoder_version(&self) -> &'static str {
|
||||
return env!("CARGO_PKG_VERSION");
|
||||
}
|
||||
|
||||
fn program_ids(&self) -> &'static [&'static str] {
|
||||
return LEGACY_PROGRAM_IDS;
|
||||
}
|
||||
|
||||
fn supports_observation(
|
||||
&self,
|
||||
observation: &crate::ProgramObservation,
|
||||
) -> crate::DecoderSupport {
|
||||
return if crate::ProtocolDecoder::handles_program_id(self, &observation.program_id) {
|
||||
crate::DecoderSupport::Maybe
|
||||
} else {
|
||||
crate::DecoderSupport::No
|
||||
};
|
||||
}
|
||||
|
||||
fn decode_observation(
|
||||
&self,
|
||||
_observation: &crate::ProgramObservation,
|
||||
) -> kb_core::Result<std::vec::Vec<crate::DecodedProtocolEvent>> {
|
||||
tracing::debug!(target: crate::SOLANA_CORE_TRACING_TARGET, "legacy native decoder adapter classified an observation without maximal decoding");
|
||||
return std::result::Result::Ok(std::vec::Vec::new());
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::InstructionDecoder for crate::SolanaCoreDecoder {
|
||||
fn identity(&self) -> crate::DecoderIdentity {
|
||||
return crate::DecoderIdentity {
|
||||
name: "solana_native_classifier".to_string(),
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
fn surfaces(&self) -> &'static [crate::DecoderSurface] {
|
||||
return NATIVE_SURFACES;
|
||||
}
|
||||
|
||||
fn coverage(&self) -> std::vec::Vec<crate::DecoderCoverageDeclaration> {
|
||||
let mut coverage = crate::solana_core_system_coverage();
|
||||
coverage.extend(crate::solana_core_compute_budget_coverage());
|
||||
coverage.extend(crate::solana_core_config_coverage());
|
||||
coverage.extend(crate::solana_core_feature_coverage());
|
||||
coverage.extend(crate::solana_core_vote_coverage());
|
||||
coverage.extend(crate::solana_core_stake_coverage());
|
||||
coverage.extend(crate::solana_core_address_lookup_table_coverage());
|
||||
coverage.extend(crate::solana_core_loaders_coverage());
|
||||
coverage.extend(crate::solana_core_precompiles_coverage());
|
||||
coverage.extend(crate::solana_core_slashing_coverage());
|
||||
coverage.extend(crate::solana_core_zk_elgamal_coverage());
|
||||
coverage.extend(crate::solana_core_zk_token_proof_coverage());
|
||||
for surface in NATIVE_SURFACES {
|
||||
if surface.program_id == kb_program_ids::SYSTEM_PROGRAM_ID
|
||||
|| surface.program_id == kb_program_ids::COMPUTE_BUDGET_PROGRAM_ID
|
||||
|| surface.program_id == kb_program_ids::ADDRESS_LOOKUP_TABLE_PROGRAM_ID
|
||||
|| surface.program_id == kb_program_ids::CONFIG_PROGRAM_ID
|
||||
|| surface.program_id == kb_program_ids::FEATURE_PROGRAM_ID
|
||||
|| surface.program_id == kb_program_ids::VOTE_PROGRAM_ID
|
||||
|| surface.program_id == kb_program_ids::STAKE_PROGRAM_ID
|
||||
|| is_signature_precompile(surface.program_id)
|
||||
|| surface.program_id == kb_program_ids::SLASHING_PROGRAM_ID
|
||||
|| surface.program_id == kb_program_ids::ZK_ELGAMAL_PROOF_PROGRAM_ID
|
||||
|| surface.program_id == kb_program_ids::ZK_TOKEN_PROOF_PROGRAM_ID
|
||||
|| is_loader_program(surface.program_id)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
coverage.push(crate::DecoderCoverageDeclaration {
|
||||
program_id: surface.program_id.to_string(),
|
||||
surface_code: std::option::Option::Some(surface.surface_code.to_string()),
|
||||
entry_kind: crate::DecoderCoverageEntryKind::Instruction,
|
||||
entry_code: "unclassified_native_instruction".to_string(),
|
||||
discriminator_hex: std::option::Option::None,
|
||||
historical: is_historical_native_program(surface.program_id),
|
||||
});
|
||||
}
|
||||
return coverage;
|
||||
}
|
||||
|
||||
fn recognize(&self, input: &crate::CoreInstructionReplayInput) -> crate::DecoderRecognition {
|
||||
tracing::debug!(target: crate::SOLANA_CORE_TRACING_TARGET, action = "recognize", signature = %input.signature, slot = input.slot, instruction_path = %input.instruction_path, program_id = %input.program_id, input_key = %input.replay_input_key, transaction_failed = input.transaction_failed, "recognize native Solana contextual instruction");
|
||||
let surface = NATIVE_SURFACES
|
||||
.iter()
|
||||
.find(|surface| return surface.program_id == input.program_id);
|
||||
let surface = match surface {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
tracing::debug!(target: crate::SOLANA_CORE_TRACING_TARGET, action = "recognize", signature = %input.signature, instruction_path = %input.instruction_path, program_id = %input.program_id, compatible = false, "native Solana decoder does not support program id");
|
||||
return crate::DecoderRecognition::incompatible();
|
||||
},
|
||||
};
|
||||
let recognition = if input.program_id == kb_program_ids::SYSTEM_PROGRAM_ID {
|
||||
crate::solana_core_system_recognize(input, surface.priority)
|
||||
} else if input.program_id == kb_program_ids::COMPUTE_BUDGET_PROGRAM_ID {
|
||||
crate::solana_core_compute_budget_recognize(input, surface.priority)
|
||||
} else if input.program_id == kb_program_ids::ADDRESS_LOOKUP_TABLE_PROGRAM_ID {
|
||||
crate::solana_core_address_lookup_table_recognize(input, surface.priority)
|
||||
} else if input.program_id == kb_program_ids::CONFIG_PROGRAM_ID {
|
||||
crate::solana_core_config_recognize(input, surface.priority)
|
||||
} else if input.program_id == kb_program_ids::FEATURE_PROGRAM_ID {
|
||||
crate::solana_core_feature_recognize(input, surface.priority)
|
||||
} else if input.program_id == kb_program_ids::VOTE_PROGRAM_ID {
|
||||
crate::solana_core_vote_recognize(input, surface.priority)
|
||||
} else if input.program_id == kb_program_ids::STAKE_PROGRAM_ID {
|
||||
crate::solana_core_stake_recognize(input, surface.priority)
|
||||
} else if input.program_id == kb_program_ids::SLASHING_PROGRAM_ID {
|
||||
crate::solana_core_slashing_recognize(input, surface.priority)
|
||||
} else if is_signature_precompile(input.program_id.as_str()) {
|
||||
crate::solana_core_precompiles_recognize(input, surface.priority)
|
||||
} else if input.program_id == kb_program_ids::ZK_ELGAMAL_PROOF_PROGRAM_ID {
|
||||
crate::solana_core_zk_elgamal_recognize(input, surface.priority)
|
||||
} else if input.program_id == kb_program_ids::ZK_TOKEN_PROOF_PROGRAM_ID {
|
||||
crate::solana_core_zk_token_proof_recognize(input, surface.priority)
|
||||
} else if is_loader_program(input.program_id.as_str()) {
|
||||
crate::solana_core_loaders_recognize(input, surface.priority)
|
||||
} else {
|
||||
crate::DecoderRecognition::compatible(
|
||||
false,
|
||||
surface.priority,
|
||||
std::option::Option::Some(surface.surface_code.to_string()),
|
||||
std::option::Option::Some("unclassified_native_instruction".to_string()),
|
||||
crate::discriminator_8_hex(input),
|
||||
)
|
||||
};
|
||||
tracing::debug!(target: crate::SOLANA_CORE_TRACING_TARGET, action = "recognize", signature = %input.signature, instruction_path = %input.instruction_path, program_id = %input.program_id, surface_code = surface.surface_code, recognition = ?recognition, compatible = true, "native Solana decoder recognized contextual instruction");
|
||||
return recognition;
|
||||
}
|
||||
|
||||
fn decode(&self, input: &crate::CoreInstructionReplayInput) -> crate::DecoderExecutionResult {
|
||||
let result = if input.program_id == kb_program_ids::SYSTEM_PROGRAM_ID {
|
||||
crate::solana_core_system_decode(input)
|
||||
} else if input.program_id == kb_program_ids::COMPUTE_BUDGET_PROGRAM_ID {
|
||||
crate::solana_core_compute_budget_decode(input)
|
||||
} else if input.program_id == kb_program_ids::ADDRESS_LOOKUP_TABLE_PROGRAM_ID {
|
||||
crate::solana_core_address_lookup_table_decode(input)
|
||||
} else if input.program_id == kb_program_ids::CONFIG_PROGRAM_ID {
|
||||
crate::solana_core_config_decode(input)
|
||||
} else if input.program_id == kb_program_ids::FEATURE_PROGRAM_ID {
|
||||
crate::solana_core_feature_decode(input)
|
||||
} else if input.program_id == kb_program_ids::VOTE_PROGRAM_ID {
|
||||
crate::solana_core_vote_decode(input)
|
||||
} else if input.program_id == kb_program_ids::STAKE_PROGRAM_ID {
|
||||
crate::solana_core_stake_decode(input)
|
||||
} else if input.program_id == kb_program_ids::SLASHING_PROGRAM_ID {
|
||||
crate::solana_core_slashing_decode(input)
|
||||
} else if is_signature_precompile(input.program_id.as_str()) {
|
||||
crate::solana_core_precompiles_decode(input)
|
||||
} else if input.program_id == kb_program_ids::ZK_ELGAMAL_PROOF_PROGRAM_ID {
|
||||
crate::solana_core_zk_elgamal_decode(input)
|
||||
} else if input.program_id == kb_program_ids::ZK_TOKEN_PROOF_PROGRAM_ID {
|
||||
crate::solana_core_zk_token_proof_decode(input)
|
||||
} else if is_loader_program(input.program_id.as_str()) {
|
||||
crate::solana_core_loaders_decode(input)
|
||||
} else {
|
||||
crate::DecoderExecutionResult::unsupported(std::option::Option::Some(
|
||||
"unclassified_native_instruction".to_string(),
|
||||
))
|
||||
};
|
||||
if matches!(
|
||||
result.status,
|
||||
crate::DecoderOutcomeStatus::Failed | crate::DecoderOutcomeStatus::Unsupported
|
||||
) {
|
||||
tracing::error!(
|
||||
target: crate::SOLANA_CORE_TRACING_TARGET,
|
||||
action = "decode_failure",
|
||||
signature = %input.signature,
|
||||
slot = input.slot,
|
||||
instruction_path = %input.instruction_path,
|
||||
program_id = %input.program_id,
|
||||
processor_name = "solana_native_classifier",
|
||||
processor_version = env!("CARGO_PKG_VERSION"),
|
||||
input_key = %input.replay_input_key,
|
||||
payload_hash = ?input.instruction_payload_hash,
|
||||
payload_length = ?crate::solana_core_decoded_payload_length(input),
|
||||
transaction_failed = input.transaction_failed,
|
||||
transaction_error = ?input.transaction_err_json,
|
||||
result_status = ?result.status,
|
||||
recognized_entry_code = ?result.recognized_entry_code,
|
||||
diagnostics = ?result.diagnostics,
|
||||
"native instruction was not decoded successfully"
|
||||
);
|
||||
}
|
||||
tracing::debug!(target: crate::SOLANA_CORE_TRACING_TARGET, action = "decode", signature = %input.signature, slot = input.slot, instruction_path = %input.instruction_path, program_id = %input.program_id, input_key = %input.replay_input_key, payload_hash = ?input.instruction_payload_hash, transaction_failed = input.transaction_failed, transaction_error = ?input.transaction_err_json, result_status = ?result.status, recognized_entry_code = ?result.recognized_entry_code, observation_count = result.observations.len(), diagnostic_count = result.diagnostics.len(), "native instruction decode completed");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
fn is_signature_precompile(program_id: &str) -> bool {
|
||||
return program_id == kb_program_ids::ED25519_PROGRAM_ID
|
||||
|| program_id == kb_program_ids::SECP256K1_PROGRAM_ID
|
||||
|| program_id == kb_program_ids::SECP256R1_PROGRAM_ID;
|
||||
}
|
||||
|
||||
fn is_loader_program(program_id: &str) -> bool {
|
||||
return program_id == kb_program_ids::BPF_LOADER_DEPRECATED_PROGRAM_ID
|
||||
|| program_id == kb_program_ids::BPF_LOADER_PROGRAM_ID
|
||||
|| program_id == kb_program_ids::BPF_LOADER_UPGRADEABLE_PROGRAM_ID
|
||||
|| program_id == kb_program_ids::LOADER_V4_PROGRAM_ID
|
||||
|| program_id == kb_program_ids::NATIVE_LOADER_PROGRAM_ID;
|
||||
}
|
||||
|
||||
fn is_historical_native_program(program_id: &str) -> bool {
|
||||
return program_id == kb_program_ids::BPF_LOADER_DEPRECATED_PROGRAM_ID
|
||||
|| program_id == kb_program_ids::BPF_LOADER_PROGRAM_ID
|
||||
|| program_id == kb_program_ids::ZK_TOKEN_PROOF_PROGRAM_ID;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
fn replay_input(program_id: &str) -> crate::CoreInstructionReplayInput {
|
||||
let result = crate::CoreInstructionReplayInput::new(
|
||||
"signature:0",
|
||||
"signature",
|
||||
42,
|
||||
"0",
|
||||
program_id,
|
||||
false,
|
||||
std::option::Option::None,
|
||||
serde_json::json!([]),
|
||||
serde_json::json!([]),
|
||||
std::option::Option::Some(serde_json::json!({"dataBase64": "AQIDBAUGBwg="})),
|
||||
std::option::Option::Some("payload-hash".to_string()),
|
||||
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!("replay input failed: {error}"),
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vote_program_is_dispatched_without_false_decode_for_unknown_tag() {
|
||||
let decoder = crate::SolanaCoreDecoder;
|
||||
let input = replay_input(kb_program_ids::VOTE_PROGRAM_ID);
|
||||
let recognition = crate::InstructionDecoder::recognize(&decoder, &input);
|
||||
assert!(recognition.compatible);
|
||||
assert_eq!(
|
||||
recognition.surface_code.as_deref(),
|
||||
std::option::Option::Some("solana_native_vote")
|
||||
);
|
||||
assert_eq!(
|
||||
recognition.entry_code.as_deref(),
|
||||
std::option::Option::Some("unknown_vote_instruction")
|
||||
);
|
||||
let result = crate::InstructionDecoder::decode(&decoder, &input);
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Unsupported);
|
||||
assert!(result.observations.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stake_program_is_dispatched_without_false_decode_for_unknown_tag() {
|
||||
let decoder = crate::SolanaCoreDecoder;
|
||||
let input = replay_input(kb_program_ids::STAKE_PROGRAM_ID);
|
||||
let recognition = crate::InstructionDecoder::recognize(&decoder, &input);
|
||||
assert!(recognition.compatible);
|
||||
assert_eq!(
|
||||
recognition.surface_code.as_deref(),
|
||||
std::option::Option::Some("solana_native_stake")
|
||||
);
|
||||
assert_eq!(
|
||||
recognition.entry_code.as_deref(),
|
||||
std::option::Option::Some("unknown_stake_instruction")
|
||||
);
|
||||
let result = crate::InstructionDecoder::decode(&decoder, &input);
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Unsupported);
|
||||
assert!(result.observations.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zk_token_proof_program_dispatches_to_current_noop_fallback() {
|
||||
let decoder = crate::SolanaCoreDecoder;
|
||||
let input = replay_input(kb_program_ids::ZK_TOKEN_PROOF_PROGRAM_ID);
|
||||
let recognition = crate::InstructionDecoder::recognize(&decoder, &input);
|
||||
assert!(recognition.compatible);
|
||||
assert_eq!(
|
||||
recognition.surface_code.as_deref(),
|
||||
std::option::Option::Some("solana_native_zk_token_proof")
|
||||
);
|
||||
assert_eq!(
|
||||
recognition.entry_code.as_deref(),
|
||||
std::option::Option::Some("current_runtime_noop_invocation")
|
||||
);
|
||||
let result = crate::InstructionDecoder::decode(&decoder, &input);
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
assert_eq!(
|
||||
result.recognized_entry_code.as_deref(),
|
||||
std::option::Option::Some("current_runtime_noop_invocation")
|
||||
);
|
||||
assert_eq!(result.observations.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_native_registry_program_has_one_surface() {
|
||||
let decoder = crate::SolanaCoreDecoder;
|
||||
let surfaces = crate::InstructionDecoder::surfaces(&decoder);
|
||||
assert_eq!(surfaces.len(), kb_program_ids::native_program_ids().len());
|
||||
for entry in kb_program_ids::native_program_ids() {
|
||||
assert!(surfaces.iter().any(|surface| return surface.program_id == entry.program_id()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_native_surface_has_declared_coverage_without_fallback_entries() {
|
||||
let decoder = crate::SolanaCoreDecoder;
|
||||
let surfaces = crate::InstructionDecoder::surfaces(&decoder);
|
||||
let coverage = crate::InstructionDecoder::coverage(&decoder);
|
||||
for surface in surfaces {
|
||||
assert!(coverage.iter().any(|entry| {
|
||||
return entry.program_id == surface.program_id
|
||||
&& entry.surface_code.as_deref()
|
||||
== std::option::Option::Some(surface.surface_code);
|
||||
}));
|
||||
}
|
||||
assert!(!coverage.iter().any(|entry| {
|
||||
return entry.entry_code == "unclassified_native_instruction";
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coverage_entries_are_unique_and_reference_declared_surfaces() {
|
||||
let decoder = crate::SolanaCoreDecoder;
|
||||
let surfaces = crate::InstructionDecoder::surfaces(&decoder);
|
||||
let coverage = crate::InstructionDecoder::coverage(&decoder);
|
||||
let mut identities = std::collections::BTreeSet::new();
|
||||
for entry in coverage {
|
||||
let surface_code = match entry.surface_code.as_deref() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => panic!("native coverage entry has no surface code"),
|
||||
};
|
||||
assert!(surfaces.iter().any(|surface| {
|
||||
return surface.program_id == entry.program_id
|
||||
&& surface.surface_code == surface_code;
|
||||
}));
|
||||
assert!(identities.insert(format!(
|
||||
"{}:{surface_code}:{}:{:?}",
|
||||
entry.program_id, entry.entry_code, entry.entry_kind
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slashing_program_is_dispatched_to_exact_decoder() {
|
||||
let decoder = crate::SolanaCoreDecoder;
|
||||
let input = replay_input(kb_program_ids::SLASHING_PROGRAM_ID);
|
||||
let recognition = crate::InstructionDecoder::recognize(&decoder, &input);
|
||||
assert!(recognition.compatible);
|
||||
assert_eq!(
|
||||
recognition.surface_code.as_deref(),
|
||||
std::option::Option::Some("solana_native_slashing")
|
||||
);
|
||||
assert_eq!(
|
||||
recognition.entry_code.as_deref(),
|
||||
std::option::Option::Some("duplicate_block_proof")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deprecated_stake_config_account_is_not_an_instruction_surface() {
|
||||
let decoder = crate::SolanaCoreDecoder;
|
||||
let surfaces = crate::InstructionDecoder::surfaces(&decoder);
|
||||
assert!(!surfaces.iter().any(|surface| {
|
||||
return surface.program_id == kb_program_ids::STAKE_CONFIG_ACCOUNT_ID;
|
||||
}));
|
||||
let input = replay_input(kb_program_ids::STAKE_CONFIG_ACCOUNT_ID);
|
||||
let recognition = crate::InstructionDecoder::recognize(&decoder, &input);
|
||||
assert!(!recognition.compatible);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_decoder_matrix_matches_registry_surfaces_and_coverage() {
|
||||
let raw = include_str!("../../../../../docs/NATIVE_SOLANA_DECODER_MATRIX.json");
|
||||
let parsed = match serde_json::from_str::<serde_json::Value>(raw) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
panic!("native decoder matrix parsing failed: {error}")
|
||||
},
|
||||
};
|
||||
let matrix_surfaces = match parsed.get("surfaces").and_then(serde_json::Value::as_array) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => panic!("native decoder matrix surfaces missing"),
|
||||
};
|
||||
assert_eq!(matrix_surfaces.len(), 18);
|
||||
let declared_surface_count =
|
||||
parsed.get("surface_count").and_then(serde_json::Value::as_u64);
|
||||
assert_eq!(declared_surface_count, std::option::Option::Some(18));
|
||||
|
||||
let decoder = crate::SolanaCoreDecoder;
|
||||
let compiled_surfaces = crate::InstructionDecoder::surfaces(&decoder);
|
||||
let compiled_surface_map = compiled_surfaces
|
||||
.iter()
|
||||
.map(|surface| {
|
||||
return (surface.program_id.to_string(), surface.surface_code.to_string());
|
||||
})
|
||||
.collect::<std::collections::BTreeMap<_, _>>();
|
||||
assert_eq!(compiled_surface_map.len(), 18);
|
||||
|
||||
let native_registry = kb_program_ids::native_program_ids()
|
||||
.iter()
|
||||
.map(|entry| return entry.program_id().to_string())
|
||||
.collect::<std::collections::BTreeSet<_>>();
|
||||
assert_eq!(native_registry.len(), 18);
|
||||
|
||||
let coverage = crate::InstructionDecoder::coverage(&decoder);
|
||||
let mut compiled_coverage_counts = std::collections::BTreeMap::new();
|
||||
for entry in &coverage {
|
||||
let count = compiled_coverage_counts.entry(entry.program_id.clone()).or_insert(0_usize);
|
||||
*count = count.saturating_add(1);
|
||||
}
|
||||
|
||||
let mut matrix_surface_map = std::collections::BTreeMap::new();
|
||||
let mut matrix_program_ids = std::collections::BTreeSet::new();
|
||||
let mut matrix_coverage_total = 0_usize;
|
||||
for surface in matrix_surfaces {
|
||||
let surface_code = match surface.get("surface_code").and_then(serde_json::Value::as_str)
|
||||
{
|
||||
std::option::Option::Some(value) if !value.is_empty() => value,
|
||||
_ => panic!("native decoder matrix surface_code missing"),
|
||||
};
|
||||
let program_id = match surface.get("program_id").and_then(serde_json::Value::as_str) {
|
||||
std::option::Option::Some(value) if !value.is_empty() => value,
|
||||
_ => panic!("native decoder matrix program_id missing"),
|
||||
};
|
||||
for field in ["runtime_status", "source_contract", "completeness_test"] {
|
||||
let value = surface.get(field).and_then(serde_json::Value::as_str);
|
||||
assert!(matches!(value, std::option::Option::Some(text) if !text.is_empty()));
|
||||
}
|
||||
let expected_count_u64 = match surface
|
||||
.get("expected_coverage_entries")
|
||||
.and_then(serde_json::Value::as_u64)
|
||||
{
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
panic!("native decoder matrix expected coverage count missing")
|
||||
},
|
||||
};
|
||||
let expected_count = match usize::try_from(expected_count_u64) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
panic!("native decoder matrix coverage count invalid: {error}")
|
||||
},
|
||||
};
|
||||
assert!(expected_count > 0);
|
||||
assert!(matrix_program_ids.insert(program_id.to_string()));
|
||||
assert!(
|
||||
matrix_surface_map
|
||||
.insert(program_id.to_string(), surface_code.to_string())
|
||||
.is_none()
|
||||
);
|
||||
assert_eq!(
|
||||
compiled_coverage_counts.get(program_id).copied(),
|
||||
std::option::Option::Some(expected_count)
|
||||
);
|
||||
matrix_coverage_total = matrix_coverage_total.saturating_add(expected_count);
|
||||
}
|
||||
|
||||
let declared_coverage_count =
|
||||
parsed.get("coverage_entry_count").and_then(serde_json::Value::as_u64);
|
||||
assert_eq!(declared_coverage_count, std::option::Option::Some(121));
|
||||
assert_eq!(matrix_coverage_total, 121);
|
||||
assert_eq!(coverage.len(), matrix_coverage_total);
|
||||
assert_eq!(matrix_surface_map, compiled_surface_map);
|
||||
assert_eq!(matrix_program_ids, native_registry);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unrelated_program_is_not_dispatched() {
|
||||
let decoder = crate::SolanaCoreDecoder;
|
||||
let input = replay_input(kb_program_ids::SPL_TOKEN_PROGRAM_ID);
|
||||
let recognition = crate::InstructionDecoder::recognize(&decoder, &input);
|
||||
assert!(!recognition.compatible);
|
||||
}
|
||||
}
|
||||
101
kb-lib/src/decoder/solana/core/event.rs
Normal file
101
kb-lib/src/decoder/solana/core/event.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
// file: kb-lib/src/decoder/solana/core/event.rs
|
||||
// version: 3
|
||||
|
||||
//! Stable native event and decoder result builders.
|
||||
|
||||
/// Builds one exact decoded native observation.
|
||||
pub(crate) fn decoded_result(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
surface_code: &str,
|
||||
entry_code: &str,
|
||||
event_family: crate::EventFamily,
|
||||
historical: bool,
|
||||
accounts: serde_json::Value,
|
||||
parameters: serde_json::Value,
|
||||
source_evidence: &str,
|
||||
) -> crate::DecoderExecutionResult {
|
||||
let event = crate::DecodedProtocolEvent {
|
||||
signature: crate::Signature(input.signature.clone()),
|
||||
slot: crate::Slot(input.slot),
|
||||
instruction_path: crate::InstructionPath(input.instruction_path.clone()),
|
||||
program_id: crate::ProgramId(input.program_id.clone()),
|
||||
protocol_code: crate::ProtocolCode(crate::SOLANA_CORE_PROTOCOL_CODE.to_string()),
|
||||
surface_code: crate::SurfaceCode(surface_code.to_string()),
|
||||
event_code: crate::EventCode(format!("{surface_code}.{entry_code}")),
|
||||
event_name: crate::EventName(entry_code.to_string()),
|
||||
event_family,
|
||||
source_kind: if input.instruction_path.contains('/') {
|
||||
crate::EventSourceKind::InnerInstruction
|
||||
} else {
|
||||
crate::EventSourceKind::Instruction
|
||||
},
|
||||
confidence: crate::DecoderConfidence::ManualExact,
|
||||
};
|
||||
let payload_hash = crate::solana_core_payload_hash(input);
|
||||
let observation = crate::DecodedObservation {
|
||||
event_key: format!("{entry_code}:0"),
|
||||
event,
|
||||
payload_json: serde_json::json!({
|
||||
"eventVersion": crate::SOLANA_CORE_NATIVE_EVENT_VERSION,
|
||||
"programId": input.program_id,
|
||||
"surfaceCode": surface_code,
|
||||
"entryCode": entry_code,
|
||||
"historical": historical,
|
||||
"instructionPath": input.instruction_path,
|
||||
"transactionSucceeded": !input.transaction_failed,
|
||||
"payloadHash": payload_hash,
|
||||
"accounts": accounts,
|
||||
"parameters": parameters,
|
||||
}),
|
||||
transaction_failed: input.transaction_failed,
|
||||
transaction_error: input.transaction_err_json.clone(),
|
||||
observation_committed: !input.transaction_failed,
|
||||
proof: crate::DecoderProof {
|
||||
kind: crate::DecoderProofKind::Manual,
|
||||
confidence: crate::DecoderConfidence::ManualExact,
|
||||
evidence: vec![source_evidence.to_string(), format!("payload_sha256:{payload_hash}")],
|
||||
},
|
||||
};
|
||||
return crate::DecoderExecutionResult {
|
||||
status: crate::DecoderOutcomeStatus::Decoded,
|
||||
recognized_entry_code: std::option::Option::Some(entry_code.to_string()),
|
||||
observations: vec![observation],
|
||||
diagnostics: std::vec::Vec::new(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Builds one failed native decode result.
|
||||
pub(crate) fn failed_result(
|
||||
entry_code: std::option::Option<&str>,
|
||||
code: &str,
|
||||
message: impl std::convert::Into<std::string::String>,
|
||||
) -> crate::DecoderExecutionResult {
|
||||
return crate::DecoderExecutionResult {
|
||||
status: crate::DecoderOutcomeStatus::Failed,
|
||||
recognized_entry_code: entry_code.map(str::to_string),
|
||||
observations: std::vec::Vec::new(),
|
||||
diagnostics: vec![crate::DecoderDiagnostic {
|
||||
code: code.to_string(),
|
||||
message: message.into(),
|
||||
retriable: false,
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
/// Builds one unsupported native decode result with a bounded diagnostic.
|
||||
pub(crate) fn unsupported_result(
|
||||
entry_code: &str,
|
||||
code: &str,
|
||||
message: impl std::convert::Into<std::string::String>,
|
||||
) -> crate::DecoderExecutionResult {
|
||||
return crate::DecoderExecutionResult {
|
||||
status: crate::DecoderOutcomeStatus::Unsupported,
|
||||
recognized_entry_code: std::option::Option::Some(entry_code.to_string()),
|
||||
observations: std::vec::Vec::new(),
|
||||
diagnostics: vec![crate::DecoderDiagnostic {
|
||||
code: code.to_string(),
|
||||
message: message.into(),
|
||||
retriable: false,
|
||||
}],
|
||||
};
|
||||
}
|
||||
278
kb-lib/src/decoder/solana/core/feature.rs
Normal file
278
kb-lib/src/decoder/solana/core/feature.rs
Normal file
@@ -0,0 +1,278 @@
|
||||
// file: kb-lib/src/decoder/solana/core/feature.rs
|
||||
// version: 4
|
||||
|
||||
//! Exact Feature Gate instruction decoding from the official interface contract.
|
||||
|
||||
const SOURCE: &str = "solana-feature-gate-interface@4.0.0 revoke_pending_activation contract";
|
||||
const REVOKE_ROLES: &[crate::SolanaCoreAccountRole] = &[
|
||||
crate::SolanaCoreAccountRole::new("feature_account", true, true),
|
||||
crate::SolanaCoreAccountRole::new("incinerator", true, false),
|
||||
crate::SolanaCoreAccountRole::new("system_program", false, false),
|
||||
];
|
||||
|
||||
/// Returns declared Feature Gate instruction coverage.
|
||||
pub(crate) fn feature_coverage() -> std::vec::Vec<crate::DecoderCoverageDeclaration> {
|
||||
return vec![crate::DecoderCoverageDeclaration {
|
||||
program_id: kb_program_ids::FEATURE_PROGRAM_ID.to_string(),
|
||||
surface_code: std::option::Option::Some(
|
||||
crate::SOLANA_CORE_FEATURE_SURFACE_CODE.to_string(),
|
||||
),
|
||||
entry_kind: crate::DecoderCoverageEntryKind::Instruction,
|
||||
entry_code: "revoke_pending_activation".to_string(),
|
||||
discriminator_hex: std::option::Option::Some("00".to_string()),
|
||||
historical: false,
|
||||
}];
|
||||
}
|
||||
|
||||
/// Recognizes one Feature Gate instruction without producing an event.
|
||||
pub(crate) fn feature_recognize(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
priority: u16,
|
||||
) -> crate::DecoderRecognition {
|
||||
let bytes_result = crate::solana_core_decode_instruction_data(input);
|
||||
let bytes = match bytes_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_error) => {
|
||||
return crate::DecoderRecognition::compatible(
|
||||
false,
|
||||
priority,
|
||||
std::option::Option::Some(crate::SOLANA_CORE_FEATURE_SURFACE_CODE.to_string()),
|
||||
std::option::Option::Some("malformed_feature_instruction".to_string()),
|
||||
std::option::Option::None,
|
||||
);
|
||||
},
|
||||
};
|
||||
let entry_code = match bytes.first() {
|
||||
std::option::Option::Some(0) => "revoke_pending_activation",
|
||||
std::option::Option::Some(_value) => "unknown_feature_instruction",
|
||||
std::option::Option::None => "malformed_feature_instruction",
|
||||
};
|
||||
return crate::DecoderRecognition::compatible(
|
||||
bytes.as_slice() == [0],
|
||||
priority,
|
||||
std::option::Option::Some(crate::SOLANA_CORE_FEATURE_SURFACE_CODE.to_string()),
|
||||
std::option::Option::Some(entry_code.to_string()),
|
||||
crate::solana_core_hexadecimal_prefix(bytes.as_slice(), bytes.len().min(1)),
|
||||
);
|
||||
}
|
||||
|
||||
/// Decodes one Feature Gate instruction.
|
||||
pub(crate) fn feature_decode(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
) -> crate::DecoderExecutionResult {
|
||||
let bytes_result = crate::solana_core_decode_instruction_data(input);
|
||||
let bytes = match bytes_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some("malformed_feature_instruction"),
|
||||
"feature_payload_invalid",
|
||||
error.to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
if bytes.is_empty() {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some("malformed_feature_instruction"),
|
||||
"feature_payload_empty",
|
||||
"Feature Gate instruction payload is empty",
|
||||
);
|
||||
}
|
||||
if bytes[0] != 0 {
|
||||
return crate::solana_core_unsupported_result(
|
||||
"unknown_feature_instruction",
|
||||
"feature_tag_unknown",
|
||||
format!(
|
||||
"unknown Feature Gate instruction tag {}; payload_sha256={}",
|
||||
bytes[0],
|
||||
crate::solana_core_payload_hash(input)
|
||||
),
|
||||
);
|
||||
}
|
||||
if bytes.len() != 1 {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some("revoke_pending_activation"),
|
||||
"feature_instruction_size_invalid",
|
||||
format!(
|
||||
"Feature Gate revoke_pending_activation requires 1 byte but received {}",
|
||||
bytes.len()
|
||||
),
|
||||
);
|
||||
}
|
||||
let accounts_result =
|
||||
crate::solana_core_resolve_accounts(input, REVOKE_ROLES, 3, std::option::Option::Some(3));
|
||||
let accounts = match accounts_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some("revoke_pending_activation"),
|
||||
"feature_accounts_invalid",
|
||||
error.to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
let fixed_accounts_result = validate_fixed_accounts(&accounts);
|
||||
if let std::result::Result::Err(error) = fixed_accounts_result {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some("revoke_pending_activation"),
|
||||
"feature_fixed_account_invalid",
|
||||
error.to_string(),
|
||||
);
|
||||
}
|
||||
return crate::solana_core_decoded_result(
|
||||
input,
|
||||
crate::SOLANA_CORE_FEATURE_SURFACE_CODE,
|
||||
"revoke_pending_activation",
|
||||
crate::EventFamily::Lifecycle,
|
||||
false,
|
||||
accounts,
|
||||
serde_json::json!({
|
||||
"burnsFeatureLamports": true,
|
||||
"requiresPendingActivation": true,
|
||||
}),
|
||||
SOURCE,
|
||||
);
|
||||
}
|
||||
|
||||
fn validate_fixed_accounts(accounts: &serde_json::Value) -> kb_core::Result<()> {
|
||||
let array = match accounts.as_array() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(
|
||||
"resolved Feature Gate accounts must be an array",
|
||||
));
|
||||
},
|
||||
};
|
||||
for (position, expected) in [
|
||||
(1_usize, kb_program_ids::INCINERATOR_PROGRAM_ID),
|
||||
(2_usize, kb_program_ids::SYSTEM_PROGRAM_ID),
|
||||
] {
|
||||
let actual = array
|
||||
.get(position)
|
||||
.and_then(|value| return value.get("accountKey"))
|
||||
.and_then(serde_json::Value::as_str);
|
||||
if actual != std::option::Option::Some(expected) {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||
"Feature Gate account {position} must be {expected}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use base64::Engine; // rust-rules: trait-import
|
||||
|
||||
fn replay_input(bytes: &[u8], transaction_failed: bool) -> crate::CoreInstructionReplayInput {
|
||||
let keys = [
|
||||
"FeatureAccount11111111111111111111111111111",
|
||||
kb_program_ids::INCINERATOR_PROGRAM_ID,
|
||||
kb_program_ids::SYSTEM_PROGRAM_ID,
|
||||
];
|
||||
let account_keys = keys
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, key)| {
|
||||
return serde_json::json!({
|
||||
"accountIndex": index,
|
||||
"accountKey": key,
|
||||
"source": "static",
|
||||
"writable": index != 2,
|
||||
"signer": index == 0,
|
||||
"executable": index == 2,
|
||||
});
|
||||
})
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
let instruction_accounts = keys
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, key)| {
|
||||
return serde_json::json!({"accountIndex": index, "accountKey": key});
|
||||
})
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
let result = crate::CoreInstructionReplayInput::new(
|
||||
"signature:0",
|
||||
"signature",
|
||||
42,
|
||||
"0",
|
||||
kb_program_ids::FEATURE_PROGRAM_ID,
|
||||
transaction_failed,
|
||||
if transaction_failed {
|
||||
std::option::Option::Some(serde_json::json!({"InstructionError": [0, "Custom"]}))
|
||||
} else {
|
||||
std::option::Option::None
|
||||
},
|
||||
serde_json::Value::Array(account_keys),
|
||||
serde_json::Value::Array(instruction_accounts),
|
||||
std::option::Option::Some(serde_json::json!({
|
||||
"dataBase64": base64::engine::general_purpose::STANDARD.encode(bytes),
|
||||
})),
|
||||
std::option::Option::Some("payload-hash".to_string()),
|
||||
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!("Feature Gate replay input failed: {error}"),
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn official_payload_is_recognized_exactly() {
|
||||
let input = replay_input(&[0], false);
|
||||
let recognition = crate::solana_core_feature_recognize(&input, 100);
|
||||
assert!(recognition.compatible);
|
||||
assert!(recognition.exact);
|
||||
assert_eq!(
|
||||
recognition.entry_code.as_deref(),
|
||||
std::option::Option::Some("revoke_pending_activation")
|
||||
);
|
||||
let unknown = crate::solana_core_feature_recognize(&replay_input(&[7], false), 100);
|
||||
assert!(unknown.compatible);
|
||||
assert!(!unknown.exact);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revoke_pending_activation_decodes_exact_official_layout() {
|
||||
let result = crate::solana_core_feature_decode(&replay_input(&[0], false));
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
assert_eq!(
|
||||
result.recognized_entry_code.as_deref(),
|
||||
std::option::Option::Some("revoke_pending_activation")
|
||||
);
|
||||
assert_eq!(result.observations[0].payload_json["parameters"]["burnsFeatureLamports"], true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_truncated_trailing_and_wrong_fixed_accounts_are_safe() {
|
||||
let empty = crate::solana_core_feature_decode(&replay_input(&[], false));
|
||||
assert_eq!(empty.status, crate::DecoderOutcomeStatus::Failed);
|
||||
let unknown = crate::solana_core_feature_decode(&replay_input(&[7], false));
|
||||
assert_eq!(unknown.status, crate::DecoderOutcomeStatus::Unsupported);
|
||||
let trailing = crate::solana_core_feature_decode(&replay_input(&[0, 1], false));
|
||||
assert_eq!(trailing.status, crate::DecoderOutcomeStatus::Failed);
|
||||
let mut wrong = replay_input(&[0], false);
|
||||
wrong.instruction_accounts_json[1]["accountKey"] = serde_json::json!("wrong");
|
||||
wrong.account_keys_json[1]["accountKey"] = serde_json::json!("wrong");
|
||||
let invalid = crate::solana_core_feature_decode(&wrong);
|
||||
assert_eq!(invalid.status, crate::DecoderOutcomeStatus::Failed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_feature_revoke_is_uncommitted() {
|
||||
let result = crate::solana_core_feature_decode(&replay_input(&[0], true));
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
assert!(!result.observations[0].observation_committed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coverage_declares_the_single_official_instruction() {
|
||||
let coverage = crate::solana_core_feature_coverage();
|
||||
assert_eq!(coverage.len(), 1);
|
||||
assert_eq!(coverage[0].entry_code, "revoke_pending_activation");
|
||||
assert_eq!(coverage[0].discriminator_hex.as_deref(), std::option::Option::Some("00"));
|
||||
}
|
||||
}
|
||||
1321
kb-lib/src/decoder/solana/core/loaders.rs
Normal file
1321
kb-lib/src/decoder/solana/core/loaders.rs
Normal file
File diff suppressed because it is too large
Load Diff
328
kb-lib/src/decoder/solana/core/payload.rs
Normal file
328
kb-lib/src/decoder/solana/core/payload.rs
Normal file
@@ -0,0 +1,328 @@
|
||||
// file: kb-lib/src/decoder/solana/core/payload.rs
|
||||
// version: 6
|
||||
|
||||
//! Bounded native instruction payload helpers.
|
||||
|
||||
use base64::Engine; // rust-rules: trait-import
|
||||
use sha2::Digest; // rust-rules: trait-import
|
||||
|
||||
/// One resolved outer instruction payload and its provenance relative to the target instruction.
|
||||
pub(crate) struct ResolvedInstructionPayload {
|
||||
/// Numeric outer instruction index in the transaction message.
|
||||
pub(crate) instruction_index: usize,
|
||||
/// Stable outer instruction path retained by the core store.
|
||||
pub(crate) instruction_path: std::string::String,
|
||||
/// Decoded instruction data.
|
||||
pub(crate) bytes: std::vec::Vec<u8>,
|
||||
/// Stable provenance code used by decoded precompile events.
|
||||
pub(crate) provenance: &'static str,
|
||||
}
|
||||
|
||||
/// Decodes one retained base64 instruction payload with an explicit byte limit.
|
||||
pub(crate) fn decode_instruction_data(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
) -> kb_core::Result<std::vec::Vec<u8>> {
|
||||
let payload = match input.instruction_payload_json.as_ref() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(
|
||||
"native instruction payload is not retained",
|
||||
));
|
||||
},
|
||||
};
|
||||
return decode_payload_json(payload, "native instruction");
|
||||
}
|
||||
|
||||
/// Resolves an Ed25519 or secp256r1 instruction reference using the official `u16::MAX` sentinel.
|
||||
pub(crate) fn resolve_u16_instruction_payload(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
raw_instruction_index: u16,
|
||||
) -> kb_core::Result<crate::SolanaCoreResolvedInstructionPayload> {
|
||||
if raw_instruction_index == u16::MAX {
|
||||
let target_index_result = crate::solana_core_target_outer_instruction_index(input);
|
||||
let target_index = match target_index_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let bytes_result = crate::solana_core_decode_instruction_data(input);
|
||||
let bytes = match bytes_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(crate::SolanaCoreResolvedInstructionPayload {
|
||||
instruction_index: target_index,
|
||||
instruction_path: input.instruction_path.clone(),
|
||||
bytes,
|
||||
provenance: "current_instruction",
|
||||
});
|
||||
}
|
||||
return resolve_explicit_outer_instruction(input, usize::from(raw_instruction_index));
|
||||
}
|
||||
|
||||
/// Resolves a secp256k1 instruction reference. The runtime format has no current-instruction
|
||||
/// sentinel: every `u8` value is an explicit outer instruction index.
|
||||
pub(crate) fn resolve_u8_instruction_payload(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
raw_instruction_index: u8,
|
||||
) -> kb_core::Result<crate::SolanaCoreResolvedInstructionPayload> {
|
||||
return resolve_explicit_outer_instruction(input, usize::from(raw_instruction_index));
|
||||
}
|
||||
|
||||
/// Returns the numeric outer index of the target instruction.
|
||||
pub(crate) fn target_outer_instruction_index(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
) -> kb_core::Result<usize> {
|
||||
if input.instruction_path.contains('/') {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(
|
||||
"signature precompiles can only resolve outer instruction paths",
|
||||
));
|
||||
}
|
||||
let parse_result = input.instruction_path.parse::<usize>();
|
||||
return match parse_result {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::invalid_state(
|
||||
format!("target outer instruction path is not numeric: {error}"),
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
/// Extracts one exact bounded byte slice with checked arithmetic.
|
||||
pub(crate) fn bounded_slice<'a>(
|
||||
bytes: &'a [u8],
|
||||
offset: usize,
|
||||
length: usize,
|
||||
label: &str,
|
||||
) -> kb_core::Result<&'a [u8]> {
|
||||
let end = match offset.checked_add(length) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||
"{label} range overflows usize: offset={offset}, length={length}"
|
||||
)));
|
||||
},
|
||||
};
|
||||
return match bytes.get(offset..end) {
|
||||
std::option::Option::Some(value) => std::result::Result::Ok(value),
|
||||
std::option::Option::None => {
|
||||
std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||
"{label} range is outside referenced instruction: offset={offset}, length={length}, instructionLength={}",
|
||||
bytes.len()
|
||||
)))
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the decoded target payload length when retained and valid.
|
||||
pub(crate) fn decoded_payload_length(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
) -> std::option::Option<usize> {
|
||||
return crate::solana_core_decode_instruction_data(input)
|
||||
.ok()
|
||||
.map(|bytes| return bytes.len());
|
||||
}
|
||||
|
||||
/// Returns the SHA-256 of the decoded target instruction data when available.
|
||||
pub(crate) fn decoded_payload_sha256(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
) -> std::option::Option<std::string::String> {
|
||||
let bytes_result = crate::solana_core_decode_instruction_data(input);
|
||||
return match bytes_result {
|
||||
std::result::Result::Ok(bytes) => {
|
||||
std::option::Option::Some(crate::solana_core_hash_bytes(bytes.as_slice()))
|
||||
},
|
||||
std::result::Result::Err(_error) => std::option::Option::None,
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns a stable payload hash from core or computes it from retained JSON.
|
||||
pub(crate) fn payload_hash(input: &crate::CoreInstructionReplayInput) -> std::string::String {
|
||||
if let std::option::Option::Some(value) = input.instruction_payload_hash.as_ref() {
|
||||
return value.clone();
|
||||
}
|
||||
let mut value = match input.instruction_payload_json.clone() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => serde_json::Value::Null,
|
||||
};
|
||||
let hash_result = crate::deterministic_json_hash(&mut value);
|
||||
return match hash_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_error) => "payload_hash_unavailable".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Reads one little-endian `u32` from an exact byte range.
|
||||
pub(crate) fn read_u32_le(bytes: &[u8], offset: usize) -> kb_core::Result<u32> {
|
||||
let slice_result = crate::solana_core_bounded_slice(bytes, offset, 4, "native instruction u32");
|
||||
let slice = match slice_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let array_result = <[u8; 4]>::try_from(slice);
|
||||
return match array_result {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(u32::from_le_bytes(value)),
|
||||
std::result::Result::Err(_error) => std::result::Result::Err(
|
||||
kb_core::Error::invalid_state("native instruction u32 slice has an invalid length"),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/// Reads one little-endian `u64` from an exact byte range.
|
||||
pub(crate) fn read_u64_le(bytes: &[u8], offset: usize) -> kb_core::Result<u64> {
|
||||
let slice_result = crate::solana_core_bounded_slice(bytes, offset, 8, "native instruction u64");
|
||||
let slice = match slice_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let array_result = <[u8; 8]>::try_from(slice);
|
||||
return match array_result {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(u64::from_le_bytes(value)),
|
||||
std::result::Result::Err(_error) => std::result::Result::Err(
|
||||
kb_core::Error::invalid_state("native instruction u64 slice has an invalid length"),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the lowercase SHA-256 of one bounded byte slice.
|
||||
pub(crate) fn hash_bytes(bytes: &[u8]) -> std::string::String {
|
||||
let digest = sha2::Sha256::digest(bytes);
|
||||
let mut output = std::string::String::with_capacity(digest.len().saturating_mul(2));
|
||||
for byte in digest {
|
||||
output.push_str(format!("{byte:02x}").as_str());
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
/// Returns a normalized lowercase hexadecimal prefix.
|
||||
pub(crate) fn hexadecimal_prefix(
|
||||
bytes: &[u8],
|
||||
count: usize,
|
||||
) -> std::option::Option<std::string::String> {
|
||||
let selected = match bytes.get(..count) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::option::Option::None,
|
||||
};
|
||||
let mut output = std::string::String::with_capacity(count.saturating_mul(2));
|
||||
for byte in selected {
|
||||
output.push_str(format!("{byte:02x}").as_str());
|
||||
}
|
||||
return std::option::Option::Some(output);
|
||||
}
|
||||
|
||||
/// Returns a hexadecimal prefix no longer than the available byte slice.
|
||||
pub(crate) fn bounded_hexadecimal_prefix(
|
||||
bytes: &[u8],
|
||||
maximum_count: usize,
|
||||
) -> std::string::String {
|
||||
let count = std::cmp::min(bytes.len(), maximum_count);
|
||||
return match crate::solana_core_hexadecimal_prefix(bytes, count) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => std::string::String::new(),
|
||||
};
|
||||
}
|
||||
|
||||
fn resolve_explicit_outer_instruction(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
instruction_index: usize,
|
||||
) -> kb_core::Result<crate::SolanaCoreResolvedInstructionPayload> {
|
||||
let entries = match input.outer_instructions_json.as_array() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(
|
||||
"outer instruction context is not a JSON array",
|
||||
));
|
||||
},
|
||||
};
|
||||
let target_index_result = crate::solana_core_target_outer_instruction_index(input);
|
||||
let target_index = match target_index_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let entry = entries.iter().find(|entry| {
|
||||
let value = entry
|
||||
.get("instructionIndex")
|
||||
.and_then(serde_json::Value::as_u64)
|
||||
.and_then(|value| return usize::try_from(value).ok());
|
||||
return value == std::option::Option::Some(instruction_index);
|
||||
});
|
||||
let entry = match entry {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||
"referenced outer instruction index is absent: {instruction_index}"
|
||||
)));
|
||||
},
|
||||
};
|
||||
let instruction_path = match entry.get("instructionPath").and_then(serde_json::Value::as_str) {
|
||||
std::option::Option::Some(value) if !value.trim().is_empty() => value.to_string(),
|
||||
_ => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||
"referenced outer instruction {instruction_index} has no instructionPath"
|
||||
)));
|
||||
},
|
||||
};
|
||||
let payload_json = match entry.get("payloadJson") {
|
||||
std::option::Option::Some(value) if !value.is_null() => value,
|
||||
_ => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||
"referenced outer instruction {instruction_index} has no retained payloadJson"
|
||||
)));
|
||||
},
|
||||
};
|
||||
let bytes_result = decode_payload_json(
|
||||
payload_json,
|
||||
format!("referenced outer instruction {instruction_index}").as_str(),
|
||||
);
|
||||
let bytes = match bytes_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(crate::SolanaCoreResolvedInstructionPayload {
|
||||
instruction_index,
|
||||
instruction_path,
|
||||
bytes,
|
||||
provenance: if instruction_index == target_index {
|
||||
"current_instruction"
|
||||
} else {
|
||||
"outer_instruction"
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
fn decode_payload_json(
|
||||
payload: &serde_json::Value,
|
||||
label: &str,
|
||||
) -> kb_core::Result<std::vec::Vec<u8>> {
|
||||
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::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||
"{label} payload does not contain dataBase64"
|
||||
)));
|
||||
},
|
||||
};
|
||||
let maximum_encoded_length = crate::SOLANA_CORE_MAX_NATIVE_INSTRUCTION_PAYLOAD_BYTES
|
||||
.saturating_mul(4)
|
||||
.saturating_div(3)
|
||||
.saturating_add(4);
|
||||
if encoded.len() > maximum_encoded_length {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||
"{label} base64 payload exceeds {maximum_encoded_length} bytes"
|
||||
)));
|
||||
}
|
||||
let decoded_result = base64::engine::general_purpose::STANDARD.decode(encoded.as_bytes());
|
||||
let decoded = match decoded_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||
"{label} payload is not valid base64: {error}"
|
||||
)));
|
||||
},
|
||||
};
|
||||
if decoded.len() > crate::SOLANA_CORE_MAX_NATIVE_INSTRUCTION_PAYLOAD_BYTES {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||
"{label} payload exceeds {} decoded bytes",
|
||||
crate::SOLANA_CORE_MAX_NATIVE_INSTRUCTION_PAYLOAD_BYTES
|
||||
)));
|
||||
}
|
||||
return std::result::Result::Ok(decoded);
|
||||
}
|
||||
1663
kb-lib/src/decoder/solana/core/precompiles.rs
Normal file
1663
kb-lib/src/decoder/solana/core/precompiles.rs
Normal file
File diff suppressed because it is too large
Load Diff
685
kb-lib/src/decoder/solana/core/slashing.rs
Normal file
685
kb-lib/src/decoder/solana/core/slashing.rs
Normal file
@@ -0,0 +1,685 @@
|
||||
// file: kb-lib/src/decoder/solana/core/slashing.rs
|
||||
// version: 3
|
||||
|
||||
//! Exact structural decoding for the enshrined stateless Slashing Program.
|
||||
|
||||
const SOURCE: &str = "solana-program/slashing@fe8da3a instruction contract and Agave v4.1.1 stateless builtin verified build";
|
||||
const DUPLICATE_BLOCK_PROOF_DATA_BYTES: usize = 304;
|
||||
const DUPLICATE_BLOCK_PROOF_INSTRUCTION_BYTES: usize = 305;
|
||||
const SIGNATURE_BYTES: usize = 64;
|
||||
const HASH_BYTES: usize = 32;
|
||||
const PUBKEY_BYTES: usize = 32;
|
||||
const CLOSE_ROLES: &[crate::SolanaCoreAccountRole] = &[
|
||||
crate::SolanaCoreAccountRole::new("violation_report", true, false),
|
||||
crate::SolanaCoreAccountRole::new("destination", true, false),
|
||||
];
|
||||
const DUPLICATE_BLOCK_ROLES: &[crate::SolanaCoreAccountRole] = &[
|
||||
crate::SolanaCoreAccountRole::new("proof_account", false, false),
|
||||
crate::SolanaCoreAccountRole::new("violation_report", true, false),
|
||||
crate::SolanaCoreAccountRole::new("instructions_sysvar", false, false),
|
||||
crate::SolanaCoreAccountRole::new("system_program", false, false),
|
||||
];
|
||||
|
||||
/// Returns declared Slashing Program instruction coverage.
|
||||
pub(crate) fn slashing_coverage() -> std::vec::Vec<crate::DecoderCoverageDeclaration> {
|
||||
return vec![
|
||||
crate::DecoderCoverageDeclaration {
|
||||
program_id: kb_program_ids::SLASHING_PROGRAM_ID.to_string(),
|
||||
surface_code: std::option::Option::Some(
|
||||
crate::SOLANA_CORE_SLASHING_SURFACE_CODE.to_string(),
|
||||
),
|
||||
entry_kind: crate::DecoderCoverageEntryKind::Instruction,
|
||||
entry_code: "close_violation_report".to_string(),
|
||||
discriminator_hex: std::option::Option::Some("00".to_string()),
|
||||
historical: false,
|
||||
},
|
||||
crate::DecoderCoverageDeclaration {
|
||||
program_id: kb_program_ids::SLASHING_PROGRAM_ID.to_string(),
|
||||
surface_code: std::option::Option::Some(
|
||||
crate::SOLANA_CORE_SLASHING_SURFACE_CODE.to_string(),
|
||||
),
|
||||
entry_kind: crate::DecoderCoverageEntryKind::Instruction,
|
||||
entry_code: "duplicate_block_proof".to_string(),
|
||||
discriminator_hex: std::option::Option::Some("01".to_string()),
|
||||
historical: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/// Recognizes one Slashing Program instruction without producing an event.
|
||||
pub(crate) fn slashing_recognize(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
priority: u16,
|
||||
) -> crate::DecoderRecognition {
|
||||
let bytes_result = crate::solana_core_decode_instruction_data(input);
|
||||
let bytes = match bytes_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_error) => {
|
||||
return crate::DecoderRecognition::compatible(
|
||||
false,
|
||||
priority,
|
||||
std::option::Option::Some(crate::SOLANA_CORE_SLASHING_SURFACE_CODE.to_string()),
|
||||
std::option::Option::Some("malformed_slashing_instruction".to_string()),
|
||||
std::option::Option::None,
|
||||
);
|
||||
},
|
||||
};
|
||||
let (entry_code, exact) = match bytes.first() {
|
||||
std::option::Option::Some(0) => ("close_violation_report", bytes.len() == 1),
|
||||
std::option::Option::Some(1) => {
|
||||
("duplicate_block_proof", bytes.len() == DUPLICATE_BLOCK_PROOF_INSTRUCTION_BYTES)
|
||||
},
|
||||
std::option::Option::Some(_value) => ("unknown_slashing_instruction", false),
|
||||
std::option::Option::None => ("malformed_slashing_instruction", false),
|
||||
};
|
||||
return crate::DecoderRecognition::compatible(
|
||||
exact,
|
||||
priority,
|
||||
std::option::Option::Some(crate::SOLANA_CORE_SLASHING_SURFACE_CODE.to_string()),
|
||||
std::option::Option::Some(entry_code.to_string()),
|
||||
crate::solana_core_hexadecimal_prefix(bytes.as_slice(), bytes.len().min(1)),
|
||||
);
|
||||
}
|
||||
|
||||
/// Decodes one Slashing Program instruction.
|
||||
pub(crate) fn slashing_decode(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
) -> crate::DecoderExecutionResult {
|
||||
let bytes_result = crate::solana_core_decode_instruction_data(input);
|
||||
let bytes = match bytes_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some("malformed_slashing_instruction"),
|
||||
"slashing_payload_invalid",
|
||||
error.to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
let tag = match bytes.first() {
|
||||
std::option::Option::Some(value) => *value,
|
||||
std::option::Option::None => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some("malformed_slashing_instruction"),
|
||||
"slashing_payload_empty",
|
||||
"Slashing Program instruction payload is empty",
|
||||
);
|
||||
},
|
||||
};
|
||||
return match tag {
|
||||
0 => decode_close_violation_report(input, bytes.as_slice()),
|
||||
1 => decode_duplicate_block_proof(input, bytes.as_slice()),
|
||||
_ => crate::solana_core_unsupported_result(
|
||||
"unknown_slashing_instruction",
|
||||
"slashing_tag_unknown",
|
||||
format!(
|
||||
"unknown Slashing Program instruction tag {tag}; payload_sha256={}",
|
||||
crate::solana_core_payload_hash(input)
|
||||
),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
fn decode_close_violation_report(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
bytes: &[u8],
|
||||
) -> crate::DecoderExecutionResult {
|
||||
if bytes.len() != 1 {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some("close_violation_report"),
|
||||
"slashing_close_size_invalid",
|
||||
format!("close_violation_report requires exactly 1 byte but received {}", bytes.len()),
|
||||
);
|
||||
}
|
||||
let accounts_result =
|
||||
crate::solana_core_resolve_accounts(input, CLOSE_ROLES, 2, std::option::Option::None);
|
||||
let accounts = match accounts_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some("close_violation_report"),
|
||||
"slashing_close_accounts_invalid",
|
||||
error.to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
return crate::solana_core_decoded_result(
|
||||
input,
|
||||
crate::SOLANA_CORE_SLASHING_SURFACE_CODE,
|
||||
"close_violation_report",
|
||||
crate::EventFamily::Lifecycle,
|
||||
false,
|
||||
accounts,
|
||||
serde_json::json!({
|
||||
"proofType": "duplicate_block",
|
||||
"minimumRetentionEpochs": 3,
|
||||
"runtimeMutation": runtime_mutation(
|
||||
input,
|
||||
"violation_report_closed_lamports_transferred_owner_reset_to_system_program",
|
||||
),
|
||||
"transactionFinalAccountStateCaptured": false,
|
||||
}),
|
||||
SOURCE,
|
||||
);
|
||||
}
|
||||
|
||||
fn decode_duplicate_block_proof(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
bytes: &[u8],
|
||||
) -> crate::DecoderExecutionResult {
|
||||
if bytes.len() != DUPLICATE_BLOCK_PROOF_INSTRUCTION_BYTES {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some("duplicate_block_proof"),
|
||||
"slashing_duplicate_block_size_invalid",
|
||||
format!(
|
||||
"duplicate_block_proof requires {} bytes but received {}",
|
||||
DUPLICATE_BLOCK_PROOF_INSTRUCTION_BYTES,
|
||||
bytes.len()
|
||||
),
|
||||
);
|
||||
}
|
||||
let accounts_result = crate::solana_core_resolve_accounts(
|
||||
input,
|
||||
DUPLICATE_BLOCK_ROLES,
|
||||
4,
|
||||
std::option::Option::None,
|
||||
);
|
||||
let accounts = match accounts_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some("duplicate_block_proof"),
|
||||
"slashing_duplicate_block_accounts_invalid",
|
||||
error.to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
let fixed_accounts_result = validate_duplicate_block_fixed_accounts(&accounts);
|
||||
if let std::result::Result::Err(error) = fixed_accounts_result {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some("duplicate_block_proof"),
|
||||
"slashing_duplicate_block_fixed_account_invalid",
|
||||
error.to_string(),
|
||||
);
|
||||
}
|
||||
let parsed_result = parse_duplicate_block_parameters(input, bytes);
|
||||
let parameters = match parsed_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some("duplicate_block_proof"),
|
||||
"slashing_duplicate_block_data_invalid",
|
||||
error.to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
return crate::solana_core_decoded_result(
|
||||
input,
|
||||
crate::SOLANA_CORE_SLASHING_SURFACE_CODE,
|
||||
"duplicate_block_proof",
|
||||
crate::EventFamily::Audit,
|
||||
false,
|
||||
accounts,
|
||||
parameters,
|
||||
SOURCE,
|
||||
);
|
||||
}
|
||||
|
||||
fn parse_duplicate_block_parameters(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
bytes: &[u8],
|
||||
) -> kb_core::Result<serde_json::Value> {
|
||||
let offset_result = crate::solana_core_read_u64_le(bytes, 1);
|
||||
let offset = match offset_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let slot_result = crate::solana_core_read_u64_le(bytes, 9);
|
||||
let slot = match slot_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let node_pubkey_result = read_pubkey(bytes, 17, "slashing node pubkey");
|
||||
let node_pubkey = match node_pubkey_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let reporter_result = read_pubkey(bytes, 49, "slashing reporter");
|
||||
let reporter = match reporter_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let destination_result = read_pubkey(bytes, 81, "slashing destination");
|
||||
let destination = match destination_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let shred_1_root_result = crate::solana_core_bounded_slice(
|
||||
bytes,
|
||||
113,
|
||||
HASH_BYTES,
|
||||
"slashing first shred merkle root",
|
||||
);
|
||||
let shred_1_root = match shred_1_root_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let shred_1_signature_result = crate::solana_core_bounded_slice(
|
||||
bytes,
|
||||
145,
|
||||
SIGNATURE_BYTES,
|
||||
"slashing first shred signature",
|
||||
);
|
||||
let shred_1_signature = match shred_1_signature_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let shred_2_root_result = crate::solana_core_bounded_slice(
|
||||
bytes,
|
||||
209,
|
||||
HASH_BYTES,
|
||||
"slashing second shred merkle root",
|
||||
);
|
||||
let shred_2_root = match shred_2_root_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let shred_2_signature_result = crate::solana_core_bounded_slice(
|
||||
bytes,
|
||||
241,
|
||||
SIGNATURE_BYTES,
|
||||
"slashing second shred signature",
|
||||
);
|
||||
let shred_2_signature = match shred_2_signature_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(serde_json::json!({
|
||||
"proofType": "duplicate_block",
|
||||
"proofAccountOffset": offset,
|
||||
"violationSlot": slot,
|
||||
"nodePubkey": node_pubkey,
|
||||
"reporter": reporter,
|
||||
"destination": destination,
|
||||
"shred1MerkleRoot": hash_summary(shred_1_root),
|
||||
"shred1Signature": signature_summary(shred_1_signature),
|
||||
"shred2MerkleRoot": hash_summary(shred_2_root),
|
||||
"shred2Signature": signature_summary(shred_2_signature),
|
||||
"proofAccountDataCaptured": false,
|
||||
"proofAccountDataSemantics": "external_account_not_captured_by_transaction_core",
|
||||
"reportAccountMustBePrefunded": true,
|
||||
"prefundingProvenanceInspectedByDecoder": false,
|
||||
"violationReportPdaDerivationVerifiedByDecoder": false,
|
||||
"precedingEd25519Instruction": preceding_ed25519_instruction(input),
|
||||
"cryptographicVerificationPerformedByDecoder": false,
|
||||
"runtimeVerification": if input.transaction_failed {
|
||||
"not_asserted_transaction_failed"
|
||||
} else {
|
||||
"accepted_in_successful_transaction"
|
||||
},
|
||||
"runtimeMutation": runtime_mutation(
|
||||
input,
|
||||
"violation_report_pda_assigned_allocated_and_data_stored_after_duplicate_block_proof_acceptance",
|
||||
),
|
||||
"penaltyAppliedByProgram": false,
|
||||
"programSemantics": "records_verified_violation_report_for_external_consensus_enforcement",
|
||||
"instructionDataBytes": DUPLICATE_BLOCK_PROOF_DATA_BYTES,
|
||||
}));
|
||||
}
|
||||
|
||||
fn validate_duplicate_block_fixed_accounts(accounts: &serde_json::Value) -> kb_core::Result<()> {
|
||||
let array = match accounts.as_array() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(
|
||||
"resolved Slashing Program accounts must be an array",
|
||||
));
|
||||
},
|
||||
};
|
||||
for (position, expected) in [
|
||||
(2_usize, kb_program_ids::SYSVAR_INSTRUCTIONS_PROGRAM_ID),
|
||||
(3_usize, kb_program_ids::SYSTEM_PROGRAM_ID),
|
||||
] {
|
||||
let actual = array
|
||||
.get(position)
|
||||
.and_then(|value| return value.get("accountKey"))
|
||||
.and_then(serde_json::Value::as_str);
|
||||
if actual != std::option::Option::Some(expected) {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||
"Slashing Program account {position} must be {expected}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn read_pubkey(bytes: &[u8], offset: usize, label: &str) -> kb_core::Result<std::string::String> {
|
||||
let slice_result = crate::solana_core_bounded_slice(bytes, offset, PUBKEY_BYTES, label);
|
||||
return match slice_result {
|
||||
std::result::Result::Ok(value) => {
|
||||
std::result::Result::Ok(bs58::encode(value).into_string())
|
||||
},
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
fn hash_summary(bytes: &[u8]) -> serde_json::Value {
|
||||
return serde_json::json!({
|
||||
"length": bytes.len(),
|
||||
"hex": crate::solana_core_bounded_hexadecimal_prefix(bytes, bytes.len()),
|
||||
"sha256": crate::solana_core_hash_bytes(bytes),
|
||||
});
|
||||
}
|
||||
|
||||
fn signature_summary(bytes: &[u8]) -> serde_json::Value {
|
||||
return serde_json::json!({
|
||||
"length": bytes.len(),
|
||||
"prefixHex": crate::solana_core_bounded_hexadecimal_prefix(
|
||||
bytes,
|
||||
crate::SOLANA_CORE_PRECOMPILE_COMPONENT_PREFIX_BYTES,
|
||||
),
|
||||
"sha256": crate::solana_core_hash_bytes(bytes),
|
||||
});
|
||||
}
|
||||
|
||||
fn preceding_ed25519_instruction(input: &crate::CoreInstructionReplayInput) -> serde_json::Value {
|
||||
let target_index = match crate::solana_core_target_outer_instruction_index(input) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return serde_json::json!({
|
||||
"requiredRelativeInstructionOffset": -1,
|
||||
"expectedProgramId": kb_program_ids::ED25519_PROGRAM_ID,
|
||||
"resolved": false,
|
||||
"matchesExpectedProgram": false,
|
||||
"diagnostic": error.to_string(),
|
||||
});
|
||||
},
|
||||
};
|
||||
let previous_index = match target_index.checked_sub(1) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return serde_json::json!({
|
||||
"requiredRelativeInstructionOffset": -1,
|
||||
"currentInstructionIndex": target_index,
|
||||
"expectedProgramId": kb_program_ids::ED25519_PROGRAM_ID,
|
||||
"resolved": false,
|
||||
"matchesExpectedProgram": false,
|
||||
"diagnostic": "target instruction has no preceding outer instruction",
|
||||
});
|
||||
},
|
||||
};
|
||||
let previous = input.outer_instructions_json.as_array().and_then(|instructions| {
|
||||
return instructions.iter().find(|instruction| {
|
||||
return instruction
|
||||
.get("instructionIndex")
|
||||
.and_then(serde_json::Value::as_u64)
|
||||
.and_then(|value| return usize::try_from(value).ok())
|
||||
== std::option::Option::Some(previous_index);
|
||||
});
|
||||
});
|
||||
let program_id = previous
|
||||
.and_then(|instruction| return instruction.get("programId"))
|
||||
.and_then(serde_json::Value::as_str);
|
||||
let instruction_path = previous
|
||||
.and_then(|instruction| return instruction.get("instructionPath"))
|
||||
.and_then(serde_json::Value::as_str);
|
||||
return serde_json::json!({
|
||||
"requiredRelativeInstructionOffset": -1,
|
||||
"currentInstructionIndex": target_index,
|
||||
"resolvedInstructionIndex": previous_index,
|
||||
"resolvedInstructionPath": instruction_path,
|
||||
"expectedProgramId": kb_program_ids::ED25519_PROGRAM_ID,
|
||||
"resolvedProgramId": program_id,
|
||||
"resolved": previous.is_some(),
|
||||
"matchesExpectedProgram": program_id == std::option::Option::Some(kb_program_ids::ED25519_PROGRAM_ID),
|
||||
"expectedSignatureCount": 2,
|
||||
"signatureTableInspectedByDecoder": false,
|
||||
});
|
||||
}
|
||||
|
||||
fn runtime_mutation(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
successful_value: &'static str,
|
||||
) -> &'static str {
|
||||
return if input.transaction_failed {
|
||||
"not_asserted_transaction_failed"
|
||||
} else {
|
||||
successful_value
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use base64::Engine; // rust-rules: trait-import
|
||||
|
||||
fn duplicate_payload() -> std::vec::Vec<u8> {
|
||||
let mut bytes = vec![1_u8];
|
||||
bytes.extend_from_slice(&34_u64.to_le_bytes());
|
||||
bytes.extend_from_slice(&42_u64.to_le_bytes());
|
||||
bytes.extend_from_slice(&[1_u8; 32]);
|
||||
bytes.extend_from_slice(&[2_u8; 32]);
|
||||
bytes.extend_from_slice(&[3_u8; 32]);
|
||||
bytes.extend_from_slice(&[4_u8; 32]);
|
||||
bytes.extend_from_slice(&[5_u8; 64]);
|
||||
bytes.extend_from_slice(&[6_u8; 32]);
|
||||
bytes.extend_from_slice(&[7_u8; 64]);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
fn replay_input(
|
||||
bytes: &[u8],
|
||||
transaction_failed: bool,
|
||||
instruction_path: &str,
|
||||
) -> crate::CoreInstructionReplayInput {
|
||||
let keys = [
|
||||
"Proof1111111111111111111111111111111111111",
|
||||
"Report111111111111111111111111111111111111",
|
||||
kb_program_ids::SYSVAR_INSTRUCTIONS_PROGRAM_ID,
|
||||
kb_program_ids::SYSTEM_PROGRAM_ID,
|
||||
];
|
||||
let account_keys = keys
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, key)| {
|
||||
return serde_json::json!({
|
||||
"accountIndex": index,
|
||||
"accountKey": key,
|
||||
"source": "static",
|
||||
"writable": index == 1,
|
||||
"signer": false,
|
||||
"executable": index == 3,
|
||||
});
|
||||
})
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
let instruction_accounts = keys
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, key)| {
|
||||
return serde_json::json!({"accountIndex": index, "accountKey": key});
|
||||
})
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
let current_index = instruction_path.parse::<usize>().unwrap_or(2);
|
||||
let previous_index = current_index.saturating_sub(1);
|
||||
let outer = serde_json::json!([
|
||||
{
|
||||
"instructionIndex": previous_index,
|
||||
"instructionPath": previous_index.to_string(),
|
||||
"programId": kb_program_ids::ED25519_PROGRAM_ID,
|
||||
"payloadJson": {"dataBase64": ""},
|
||||
"payloadHash": "ed25519-hash"
|
||||
},
|
||||
{
|
||||
"instructionIndex": current_index,
|
||||
"instructionPath": instruction_path,
|
||||
"programId": kb_program_ids::SLASHING_PROGRAM_ID,
|
||||
"payloadJson": {
|
||||
"dataBase64": base64::engine::general_purpose::STANDARD.encode(bytes),
|
||||
},
|
||||
"payloadHash": "slashing-hash"
|
||||
}
|
||||
]);
|
||||
let result = crate::CoreInstructionReplayInput::new(
|
||||
format!("signature:{instruction_path}"),
|
||||
"signature",
|
||||
42,
|
||||
instruction_path,
|
||||
kb_program_ids::SLASHING_PROGRAM_ID,
|
||||
transaction_failed,
|
||||
if transaction_failed {
|
||||
std::option::Option::Some(serde_json::json!({"InstructionError": [2, "Custom"]}))
|
||||
} else {
|
||||
std::option::Option::None
|
||||
},
|
||||
serde_json::Value::Array(account_keys),
|
||||
serde_json::Value::Array(instruction_accounts),
|
||||
std::option::Option::Some(serde_json::json!({
|
||||
"dataBase64": base64::engine::general_purpose::STANDARD.encode(bytes),
|
||||
})),
|
||||
std::option::Option::Some("payload-hash".to_string()),
|
||||
outer,
|
||||
serde_json::json!([]),
|
||||
serde_json::json!([]),
|
||||
serde_json::json!([]),
|
||||
);
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("Slashing replay input failed: {error}"),
|
||||
};
|
||||
}
|
||||
|
||||
fn close_replay_input(transaction_failed: bool) -> crate::CoreInstructionReplayInput {
|
||||
let mut input = replay_input(&[0], transaction_failed, "0");
|
||||
input.account_keys_json = serde_json::json!([
|
||||
{
|
||||
"accountIndex": 0,
|
||||
"accountKey": "Report111111111111111111111111111111111111",
|
||||
"source": "static",
|
||||
"writable": true,
|
||||
"signer": false,
|
||||
"executable": false
|
||||
},
|
||||
{
|
||||
"accountIndex": 1,
|
||||
"accountKey": "Destination11111111111111111111111111111111",
|
||||
"source": "static",
|
||||
"writable": true,
|
||||
"signer": false,
|
||||
"executable": false
|
||||
}
|
||||
]);
|
||||
input.instruction_accounts_json = serde_json::json!([
|
||||
{"accountIndex": 0, "accountKey": "Report111111111111111111111111111111111111"},
|
||||
{"accountIndex": 1, "accountKey": "Destination11111111111111111111111111111111"}
|
||||
]);
|
||||
return input;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coverage_declares_exactly_two_official_instructions() {
|
||||
let coverage = crate::solana_core_slashing_coverage();
|
||||
assert_eq!(coverage.len(), 2);
|
||||
assert_eq!(coverage[0].entry_code, "close_violation_report");
|
||||
assert_eq!(coverage[1].entry_code, "duplicate_block_proof");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn close_violation_report_decodes_exact_runtime_contract() {
|
||||
let result = crate::solana_core_slashing_decode(&close_replay_input(false));
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
assert_eq!(
|
||||
result.recognized_entry_code.as_deref(),
|
||||
std::option::Option::Some("close_violation_report")
|
||||
);
|
||||
let parameters = &result.observations[0].payload_json["parameters"];
|
||||
assert_eq!(parameters["minimumRetentionEpochs"], 3);
|
||||
assert_eq!(
|
||||
parameters["runtimeMutation"],
|
||||
"violation_report_closed_lamports_transferred_owner_reset_to_system_program"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_block_proof_decodes_exact_official_layout() {
|
||||
let payload = duplicate_payload();
|
||||
assert_eq!(payload.len(), super::DUPLICATE_BLOCK_PROOF_INSTRUCTION_BYTES);
|
||||
let result =
|
||||
crate::solana_core_slashing_decode(&replay_input(payload.as_slice(), false, "2"));
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
let parameters = &result.observations[0].payload_json["parameters"];
|
||||
assert_eq!(parameters["proofAccountOffset"], 34);
|
||||
assert_eq!(parameters["violationSlot"], 42);
|
||||
assert_eq!(parameters["shred1Signature"]["length"], 64);
|
||||
assert_eq!(parameters["shred2MerkleRoot"]["length"], 32);
|
||||
assert_eq!(parameters["proofAccountDataCaptured"], false);
|
||||
assert_eq!(parameters["reportAccountMustBePrefunded"], true);
|
||||
assert_eq!(parameters["prefundingProvenanceInspectedByDecoder"], false);
|
||||
assert_eq!(parameters["violationReportPdaDerivationVerifiedByDecoder"], false);
|
||||
assert_eq!(parameters["cryptographicVerificationPerformedByDecoder"], false);
|
||||
assert_eq!(parameters["penaltyAppliedByProgram"], false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_block_proof_reports_preceding_ed25519_context() {
|
||||
let result = crate::solana_core_slashing_decode(&replay_input(
|
||||
duplicate_payload().as_slice(),
|
||||
false,
|
||||
"2",
|
||||
));
|
||||
let preceding =
|
||||
&result.observations[0].payload_json["parameters"]["precedingEd25519Instruction"];
|
||||
assert_eq!(preceding["resolvedInstructionIndex"], 1);
|
||||
assert_eq!(preceding["matchesExpectedProgram"], true);
|
||||
assert_eq!(preceding["expectedSignatureCount"], 2);
|
||||
assert_eq!(preceding["signatureTableInspectedByDecoder"], false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_unknown_and_trailing_payloads_fail_safely() {
|
||||
for bytes in [vec![], vec![0, 7], vec![1, 2, 3]] {
|
||||
let result =
|
||||
crate::solana_core_slashing_decode(&replay_input(bytes.as_slice(), false, "2"));
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Failed);
|
||||
}
|
||||
let unknown = crate::solana_core_slashing_decode(&replay_input(&[9], false, "2"));
|
||||
assert_eq!(unknown.status, crate::DecoderOutcomeStatus::Unsupported);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrong_fixed_accounts_fail_safely() {
|
||||
let mut input = replay_input(duplicate_payload().as_slice(), false, "2");
|
||||
input.instruction_accounts_json[2]["accountKey"] = serde_json::json!("wrong");
|
||||
input.account_keys_json[2]["accountKey"] = serde_json::json!("wrong");
|
||||
let result = crate::solana_core_slashing_decode(&input);
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Failed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_transaction_is_decoded_without_runtime_or_mutation_claim() {
|
||||
let result = crate::solana_core_slashing_decode(&replay_input(
|
||||
duplicate_payload().as_slice(),
|
||||
true,
|
||||
"2",
|
||||
));
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
assert!(!result.observations[0].observation_committed);
|
||||
let parameters = &result.observations[0].payload_json["parameters"];
|
||||
assert_eq!(parameters["runtimeVerification"], "not_asserted_transaction_failed");
|
||||
assert_eq!(parameters["runtimeMutation"], "not_asserted_transaction_failed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialization_is_deterministic() {
|
||||
let input = replay_input(duplicate_payload().as_slice(), false, "2");
|
||||
let first = crate::solana_core_slashing_decode(&input);
|
||||
let second = crate::solana_core_slashing_decode(&input);
|
||||
let first_json = match serde_json::to_string(&first) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("first serialization failed: {error}"),
|
||||
};
|
||||
let second_json = match serde_json::to_string(&second) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("second serialization failed: {error}"),
|
||||
};
|
||||
assert_eq!(first_json, second_json);
|
||||
}
|
||||
}
|
||||
1347
kb-lib/src/decoder/solana/core/stake.rs
Normal file
1347
kb-lib/src/decoder/solana/core/stake.rs
Normal file
File diff suppressed because it is too large
Load Diff
688
kb-lib/src/decoder/solana/core/system.rs
Normal file
688
kb-lib/src/decoder/solana/core/system.rs
Normal file
@@ -0,0 +1,688 @@
|
||||
// file: kb-lib/src/decoder/solana/core/system.rs
|
||||
// version: 6
|
||||
|
||||
//! Maximal current System Program instruction decoding through the official interface enum and wincode schema.
|
||||
|
||||
const SOURCE: &str = "solana-system-interface@3.2.0 SystemInstruction wincode-compatible contract";
|
||||
|
||||
const CREATE_ACCOUNT_ROLES: &[crate::SolanaCoreAccountRole] = &[
|
||||
crate::SolanaCoreAccountRole::new("funding_account", true, true),
|
||||
crate::SolanaCoreAccountRole::new("new_account", true, true),
|
||||
];
|
||||
const ASSIGN_ROLES: &[crate::SolanaCoreAccountRole] =
|
||||
&[crate::SolanaCoreAccountRole::new("assigned_account", true, true)];
|
||||
const TRANSFER_ROLES: &[crate::SolanaCoreAccountRole] = &[
|
||||
crate::SolanaCoreAccountRole::new("funding_account", true, true),
|
||||
crate::SolanaCoreAccountRole::new("recipient_account", true, false),
|
||||
];
|
||||
const CREATE_ACCOUNT_WITH_SEED_ROLES: &[crate::SolanaCoreAccountRole] = &[
|
||||
crate::SolanaCoreAccountRole::new("funding_account", true, true),
|
||||
crate::SolanaCoreAccountRole::new("new_account", true, false),
|
||||
crate::SolanaCoreAccountRole::new("base_account", false, true),
|
||||
];
|
||||
const ADVANCE_NONCE_ROLES: &[crate::SolanaCoreAccountRole] = &[
|
||||
crate::SolanaCoreAccountRole::new("nonce_account", true, false),
|
||||
crate::SolanaCoreAccountRole::new("recent_blockhashes_sysvar", false, false),
|
||||
crate::SolanaCoreAccountRole::new("nonce_authority", false, true),
|
||||
];
|
||||
const WITHDRAW_NONCE_ROLES: &[crate::SolanaCoreAccountRole] = &[
|
||||
crate::SolanaCoreAccountRole::new("nonce_account", true, false),
|
||||
crate::SolanaCoreAccountRole::new("recipient_account", true, false),
|
||||
crate::SolanaCoreAccountRole::new("recent_blockhashes_sysvar", false, false),
|
||||
crate::SolanaCoreAccountRole::new("rent_sysvar", false, false),
|
||||
crate::SolanaCoreAccountRole::new("nonce_authority", false, true),
|
||||
];
|
||||
const INITIALIZE_NONCE_ROLES: &[crate::SolanaCoreAccountRole] = &[
|
||||
crate::SolanaCoreAccountRole::new("nonce_account", true, false),
|
||||
crate::SolanaCoreAccountRole::new("recent_blockhashes_sysvar", false, false),
|
||||
crate::SolanaCoreAccountRole::new("rent_sysvar", false, false),
|
||||
];
|
||||
const AUTHORIZE_NONCE_ROLES: &[crate::SolanaCoreAccountRole] = &[
|
||||
crate::SolanaCoreAccountRole::new("nonce_account", true, false),
|
||||
crate::SolanaCoreAccountRole::new("nonce_authority", false, true),
|
||||
];
|
||||
const ALLOCATE_ROLES: &[crate::SolanaCoreAccountRole] =
|
||||
&[crate::SolanaCoreAccountRole::new("allocated_account", true, true)];
|
||||
const SEEDED_ACCOUNT_ROLES: &[crate::SolanaCoreAccountRole] = &[
|
||||
crate::SolanaCoreAccountRole::new("derived_account", true, false),
|
||||
crate::SolanaCoreAccountRole::new("base_account", false, true),
|
||||
];
|
||||
const TRANSFER_WITH_SEED_ROLES: &[crate::SolanaCoreAccountRole] = &[
|
||||
crate::SolanaCoreAccountRole::new("funding_account", true, false),
|
||||
crate::SolanaCoreAccountRole::new("base_account", false, true),
|
||||
crate::SolanaCoreAccountRole::new("recipient_account", true, false),
|
||||
];
|
||||
const UPGRADE_NONCE_ROLES: &[crate::SolanaCoreAccountRole] =
|
||||
&[crate::SolanaCoreAccountRole::new("nonce_account", true, false)];
|
||||
const CREATE_ACCOUNT_ALLOW_PREFUND_ROLES: &[crate::SolanaCoreAccountRole] = &[
|
||||
crate::SolanaCoreAccountRole::new("new_account", true, true),
|
||||
crate::SolanaCoreAccountRole::new("funding_account", true, true),
|
||||
];
|
||||
|
||||
/// Returns declared System Program instruction coverage.
|
||||
pub(crate) fn system_coverage() -> std::vec::Vec<crate::DecoderCoverageDeclaration> {
|
||||
let entries = [
|
||||
("create_account", 0_u32),
|
||||
("assign", 1_u32),
|
||||
("transfer", 2_u32),
|
||||
("create_account_with_seed", 3_u32),
|
||||
("advance_nonce_account", 4_u32),
|
||||
("withdraw_nonce_account", 5_u32),
|
||||
("initialize_nonce_account", 6_u32),
|
||||
("authorize_nonce_account", 7_u32),
|
||||
("allocate", 8_u32),
|
||||
("allocate_with_seed", 9_u32),
|
||||
("assign_with_seed", 10_u32),
|
||||
("transfer_with_seed", 11_u32),
|
||||
("upgrade_nonce_account", 12_u32),
|
||||
("create_account_allow_prefund", 13_u32),
|
||||
];
|
||||
return entries
|
||||
.into_iter()
|
||||
.map(|(entry_code, tag)| {
|
||||
return crate::DecoderCoverageDeclaration {
|
||||
program_id: kb_program_ids::SYSTEM_PROGRAM_ID.to_string(),
|
||||
surface_code: std::option::Option::Some(
|
||||
crate::SOLANA_CORE_SYSTEM_SURFACE_CODE.to_string(),
|
||||
),
|
||||
entry_kind: crate::DecoderCoverageEntryKind::Instruction,
|
||||
entry_code: entry_code.to_string(),
|
||||
discriminator_hex: std::option::Option::Some(tag_hex(tag).to_string()),
|
||||
historical: false,
|
||||
};
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
/// Recognizes one System Program instruction without producing an event.
|
||||
pub(crate) fn system_recognize(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
priority: u16,
|
||||
) -> crate::DecoderRecognition {
|
||||
let bytes_result = crate::solana_core_decode_instruction_data(input);
|
||||
let bytes = match bytes_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_error) => {
|
||||
return crate::DecoderRecognition::compatible(
|
||||
false,
|
||||
priority,
|
||||
std::option::Option::Some(crate::SOLANA_CORE_SYSTEM_SURFACE_CODE.to_string()),
|
||||
std::option::Option::Some("malformed_system_instruction".to_string()),
|
||||
std::option::Option::None,
|
||||
);
|
||||
},
|
||||
};
|
||||
let tag_result = crate::solana_core_read_u32_le(bytes.as_slice(), 0);
|
||||
let tag = match tag_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_error) => {
|
||||
return crate::DecoderRecognition::compatible(
|
||||
false,
|
||||
priority,
|
||||
std::option::Option::Some(crate::SOLANA_CORE_SYSTEM_SURFACE_CODE.to_string()),
|
||||
std::option::Option::Some("malformed_system_instruction".to_string()),
|
||||
crate::solana_core_hexadecimal_prefix(bytes.as_slice(), bytes.len().min(4)),
|
||||
);
|
||||
},
|
||||
};
|
||||
let entry_code = entry_code(tag);
|
||||
return crate::DecoderRecognition::compatible(
|
||||
entry_code.is_some(),
|
||||
priority,
|
||||
std::option::Option::Some(crate::SOLANA_CORE_SYSTEM_SURFACE_CODE.to_string()),
|
||||
entry_code.map(str::to_string).or_else(|| {
|
||||
return std::option::Option::Some("unknown_system_instruction".to_string());
|
||||
}),
|
||||
std::option::Option::Some(tag_hex(tag).to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
/// Decodes one System Program instruction.
|
||||
pub(crate) fn system_decode(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
) -> crate::DecoderExecutionResult {
|
||||
let bytes_result = crate::solana_core_decode_instruction_data(input);
|
||||
let bytes = match bytes_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some("malformed_system_instruction"),
|
||||
"system_payload_invalid",
|
||||
error.to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
let tag_result = crate::solana_core_read_u32_le(bytes.as_slice(), 0);
|
||||
let tag = match tag_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some("malformed_system_instruction"),
|
||||
"system_tag_truncated",
|
||||
error.to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
let entry_code = match entry_code(tag) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return crate::solana_core_unsupported_result(
|
||||
"unknown_system_instruction",
|
||||
"system_tag_unknown",
|
||||
format!(
|
||||
"unknown System Program instruction tag {tag}; payload_sha256={}",
|
||||
crate::solana_core_payload_hash(input)
|
||||
),
|
||||
);
|
||||
},
|
||||
};
|
||||
let instruction_result = wincode::deserialize_exact::<
|
||||
solana_system_interface::instruction::SystemInstruction,
|
||||
>(bytes.as_slice());
|
||||
let instruction = match instruction_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some(entry_code),
|
||||
"system_wincode_invalid",
|
||||
format!("cannot decode exact System Program {entry_code}: {error}"),
|
||||
);
|
||||
},
|
||||
};
|
||||
return decoded_instruction(input, instruction);
|
||||
}
|
||||
|
||||
fn decoded_instruction(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
instruction: solana_system_interface::instruction::SystemInstruction,
|
||||
) -> crate::DecoderExecutionResult {
|
||||
return match instruction {
|
||||
solana_system_interface::instruction::SystemInstruction::CreateAccount {
|
||||
lamports,
|
||||
space,
|
||||
owner,
|
||||
} => build(
|
||||
input,
|
||||
"create_account",
|
||||
crate::EventFamily::Lifecycle,
|
||||
CREATE_ACCOUNT_ROLES,
|
||||
2,
|
||||
serde_json::json!({"lamports": lamports, "space": space, "owner": owner.to_string()}),
|
||||
),
|
||||
solana_system_interface::instruction::SystemInstruction::Assign { owner } => build(
|
||||
input,
|
||||
"assign",
|
||||
crate::EventFamily::Admin,
|
||||
ASSIGN_ROLES,
|
||||
1,
|
||||
serde_json::json!({"owner": owner.to_string()}),
|
||||
),
|
||||
solana_system_interface::instruction::SystemInstruction::Transfer { lamports } => build(
|
||||
input,
|
||||
"transfer",
|
||||
crate::EventFamily::Audit,
|
||||
TRANSFER_ROLES,
|
||||
2,
|
||||
serde_json::json!({"lamports": lamports}),
|
||||
),
|
||||
solana_system_interface::instruction::SystemInstruction::CreateAccountWithSeed {
|
||||
base,
|
||||
seed,
|
||||
lamports,
|
||||
space,
|
||||
owner,
|
||||
} => build(
|
||||
input,
|
||||
"create_account_with_seed",
|
||||
crate::EventFamily::Lifecycle,
|
||||
CREATE_ACCOUNT_WITH_SEED_ROLES,
|
||||
2,
|
||||
serde_json::json!({
|
||||
"base": base.to_string(),
|
||||
"seed": seed,
|
||||
"lamports": lamports,
|
||||
"space": space,
|
||||
"owner": owner.to_string(),
|
||||
}),
|
||||
),
|
||||
solana_system_interface::instruction::SystemInstruction::AdvanceNonceAccount => build(
|
||||
input,
|
||||
"advance_nonce_account",
|
||||
crate::EventFamily::Lifecycle,
|
||||
ADVANCE_NONCE_ROLES,
|
||||
3,
|
||||
serde_json::json!({}),
|
||||
),
|
||||
solana_system_interface::instruction::SystemInstruction::WithdrawNonceAccount(lamports) => {
|
||||
build(
|
||||
input,
|
||||
"withdraw_nonce_account",
|
||||
crate::EventFamily::Lifecycle,
|
||||
WITHDRAW_NONCE_ROLES,
|
||||
5,
|
||||
serde_json::json!({"lamports": lamports}),
|
||||
)
|
||||
},
|
||||
solana_system_interface::instruction::SystemInstruction::InitializeNonceAccount(
|
||||
authority,
|
||||
) => build(
|
||||
input,
|
||||
"initialize_nonce_account",
|
||||
crate::EventFamily::Lifecycle,
|
||||
INITIALIZE_NONCE_ROLES,
|
||||
3,
|
||||
serde_json::json!({"authority": authority.to_string()}),
|
||||
),
|
||||
solana_system_interface::instruction::SystemInstruction::AuthorizeNonceAccount(
|
||||
authority,
|
||||
) => build(
|
||||
input,
|
||||
"authorize_nonce_account",
|
||||
crate::EventFamily::Admin,
|
||||
AUTHORIZE_NONCE_ROLES,
|
||||
2,
|
||||
serde_json::json!({"authority": authority.to_string()}),
|
||||
),
|
||||
solana_system_interface::instruction::SystemInstruction::Allocate { space } => build(
|
||||
input,
|
||||
"allocate",
|
||||
crate::EventFamily::Lifecycle,
|
||||
ALLOCATE_ROLES,
|
||||
1,
|
||||
serde_json::json!({"space": space}),
|
||||
),
|
||||
solana_system_interface::instruction::SystemInstruction::AllocateWithSeed {
|
||||
base,
|
||||
seed,
|
||||
space,
|
||||
owner,
|
||||
} => build(
|
||||
input,
|
||||
"allocate_with_seed",
|
||||
crate::EventFamily::Lifecycle,
|
||||
SEEDED_ACCOUNT_ROLES,
|
||||
2,
|
||||
serde_json::json!({
|
||||
"base": base.to_string(),
|
||||
"seed": seed,
|
||||
"space": space,
|
||||
"owner": owner.to_string(),
|
||||
}),
|
||||
),
|
||||
solana_system_interface::instruction::SystemInstruction::AssignWithSeed {
|
||||
base,
|
||||
seed,
|
||||
owner,
|
||||
} => build(
|
||||
input,
|
||||
"assign_with_seed",
|
||||
crate::EventFamily::Admin,
|
||||
SEEDED_ACCOUNT_ROLES,
|
||||
2,
|
||||
serde_json::json!({
|
||||
"base": base.to_string(),
|
||||
"seed": seed,
|
||||
"owner": owner.to_string(),
|
||||
}),
|
||||
),
|
||||
solana_system_interface::instruction::SystemInstruction::TransferWithSeed {
|
||||
lamports,
|
||||
from_seed,
|
||||
from_owner,
|
||||
} => build(
|
||||
input,
|
||||
"transfer_with_seed",
|
||||
crate::EventFamily::Audit,
|
||||
TRANSFER_WITH_SEED_ROLES,
|
||||
3,
|
||||
serde_json::json!({
|
||||
"lamports": lamports,
|
||||
"fromSeed": from_seed,
|
||||
"fromOwner": from_owner.to_string(),
|
||||
}),
|
||||
),
|
||||
solana_system_interface::instruction::SystemInstruction::UpgradeNonceAccount => build(
|
||||
input,
|
||||
"upgrade_nonce_account",
|
||||
crate::EventFamily::Lifecycle,
|
||||
UPGRADE_NONCE_ROLES,
|
||||
1,
|
||||
serde_json::json!({}),
|
||||
),
|
||||
solana_system_interface::instruction::SystemInstruction::CreateAccountAllowPrefund {
|
||||
lamports,
|
||||
space,
|
||||
owner,
|
||||
} => build(
|
||||
input,
|
||||
"create_account_allow_prefund",
|
||||
crate::EventFamily::Lifecycle,
|
||||
CREATE_ACCOUNT_ALLOW_PREFUND_ROLES,
|
||||
if lamports == 0 { 1 } else { 2 },
|
||||
serde_json::json!({"lamports": lamports, "space": space, "owner": owner.to_string()}),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
fn build(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
entry_code: &str,
|
||||
event_family: crate::EventFamily,
|
||||
roles: &[crate::SolanaCoreAccountRole],
|
||||
minimum_count: usize,
|
||||
parameters: serde_json::Value,
|
||||
) -> crate::DecoderExecutionResult {
|
||||
let accounts_result =
|
||||
crate::solana_core_resolve_accounts(input, roles, minimum_count, std::option::Option::None);
|
||||
let accounts = match accounts_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some(entry_code),
|
||||
"system_accounts_invalid",
|
||||
error.to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
return crate::solana_core_decoded_result(
|
||||
input,
|
||||
crate::SOLANA_CORE_SYSTEM_SURFACE_CODE,
|
||||
entry_code,
|
||||
event_family,
|
||||
false,
|
||||
accounts,
|
||||
parameters,
|
||||
SOURCE,
|
||||
);
|
||||
}
|
||||
|
||||
fn entry_code(tag: u32) -> std::option::Option<&'static str> {
|
||||
return match tag {
|
||||
0 => std::option::Option::Some("create_account"),
|
||||
1 => std::option::Option::Some("assign"),
|
||||
2 => std::option::Option::Some("transfer"),
|
||||
3 => std::option::Option::Some("create_account_with_seed"),
|
||||
4 => std::option::Option::Some("advance_nonce_account"),
|
||||
5 => std::option::Option::Some("withdraw_nonce_account"),
|
||||
6 => std::option::Option::Some("initialize_nonce_account"),
|
||||
7 => std::option::Option::Some("authorize_nonce_account"),
|
||||
8 => std::option::Option::Some("allocate"),
|
||||
9 => std::option::Option::Some("allocate_with_seed"),
|
||||
10 => std::option::Option::Some("assign_with_seed"),
|
||||
11 => std::option::Option::Some("transfer_with_seed"),
|
||||
12 => std::option::Option::Some("upgrade_nonce_account"),
|
||||
13 => std::option::Option::Some("create_account_allow_prefund"),
|
||||
_ => std::option::Option::None,
|
||||
};
|
||||
}
|
||||
|
||||
fn tag_hex(tag: u32) -> std::string::String {
|
||||
let bytes = tag.to_le_bytes();
|
||||
return format!("{:02x}{:02x}{:02x}{:02x}", bytes[0], bytes[1], bytes[2], bytes[3]);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use base64::Engine; // rust-rules: trait-import
|
||||
|
||||
fn replay_input(
|
||||
instruction: &solana_system_interface::instruction::SystemInstruction,
|
||||
account_count: usize,
|
||||
transaction_failed: bool,
|
||||
) -> crate::CoreInstructionReplayInput {
|
||||
let bytes_result = wincode::serialize(instruction);
|
||||
let bytes = match bytes_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
panic!("system fixture serialization failed: {error}")
|
||||
},
|
||||
};
|
||||
return replay_input_bytes(bytes.as_slice(), account_count, transaction_failed);
|
||||
}
|
||||
|
||||
fn replay_input_bytes(
|
||||
bytes: &[u8],
|
||||
account_count: usize,
|
||||
transaction_failed: bool,
|
||||
) -> crate::CoreInstructionReplayInput {
|
||||
let encoded = base64::engine::general_purpose::STANDARD.encode(bytes);
|
||||
let account_keys = (0..account_count)
|
||||
.map(|index| {
|
||||
return serde_json::json!({
|
||||
"accountIndex": index,
|
||||
"accountKey": format!("account{index}"),
|
||||
"source": "static",
|
||||
"writable": true,
|
||||
"signer": true,
|
||||
"executable": false,
|
||||
});
|
||||
})
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
let instruction_accounts = (0..account_count)
|
||||
.map(|index| {
|
||||
return serde_json::json!({
|
||||
"accountIndex": index,
|
||||
"accountKey": format!("account{index}"),
|
||||
});
|
||||
})
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
let result = crate::CoreInstructionReplayInput::new(
|
||||
"signature:0",
|
||||
"signature",
|
||||
42,
|
||||
"0",
|
||||
kb_program_ids::SYSTEM_PROGRAM_ID,
|
||||
transaction_failed,
|
||||
if transaction_failed {
|
||||
std::option::Option::Some(serde_json::json!({"InstructionError": [0, "Custom"]}))
|
||||
} else {
|
||||
std::option::Option::None
|
||||
},
|
||||
serde_json::Value::Array(account_keys),
|
||||
serde_json::Value::Array(instruction_accounts),
|
||||
std::option::Option::Some(serde_json::json!({"dataBase64": encoded})),
|
||||
std::option::Option::Some("payload-hash".to_string()),
|
||||
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!("replay input failed: {error}"),
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_official_variant_decodes() {
|
||||
let address = solana_sdk_ids::system_program::id();
|
||||
let fixtures = [
|
||||
(
|
||||
solana_system_interface::instruction::SystemInstruction::CreateAccount {
|
||||
lamports: u64::MAX,
|
||||
space: u64::MAX,
|
||||
owner: address,
|
||||
},
|
||||
2,
|
||||
"create_account",
|
||||
),
|
||||
(
|
||||
solana_system_interface::instruction::SystemInstruction::Assign { owner: address },
|
||||
1,
|
||||
"assign",
|
||||
),
|
||||
(
|
||||
solana_system_interface::instruction::SystemInstruction::Transfer {
|
||||
lamports: u64::MAX,
|
||||
},
|
||||
2,
|
||||
"transfer",
|
||||
),
|
||||
(
|
||||
solana_system_interface::instruction::SystemInstruction::CreateAccountWithSeed {
|
||||
base: address,
|
||||
seed: "seed".to_string(),
|
||||
lamports: 1,
|
||||
space: 2,
|
||||
owner: address,
|
||||
},
|
||||
3,
|
||||
"create_account_with_seed",
|
||||
),
|
||||
(
|
||||
solana_system_interface::instruction::SystemInstruction::AdvanceNonceAccount,
|
||||
3,
|
||||
"advance_nonce_account",
|
||||
),
|
||||
(
|
||||
solana_system_interface::instruction::SystemInstruction::WithdrawNonceAccount(1),
|
||||
5,
|
||||
"withdraw_nonce_account",
|
||||
),
|
||||
(
|
||||
solana_system_interface::instruction::SystemInstruction::InitializeNonceAccount(
|
||||
address,
|
||||
),
|
||||
3,
|
||||
"initialize_nonce_account",
|
||||
),
|
||||
(
|
||||
solana_system_interface::instruction::SystemInstruction::AuthorizeNonceAccount(
|
||||
address,
|
||||
),
|
||||
2,
|
||||
"authorize_nonce_account",
|
||||
),
|
||||
(
|
||||
solana_system_interface::instruction::SystemInstruction::Allocate { space: 2 },
|
||||
1,
|
||||
"allocate",
|
||||
),
|
||||
(
|
||||
solana_system_interface::instruction::SystemInstruction::AllocateWithSeed {
|
||||
base: address,
|
||||
seed: "seed".to_string(),
|
||||
space: 2,
|
||||
owner: address,
|
||||
},
|
||||
2,
|
||||
"allocate_with_seed",
|
||||
),
|
||||
(
|
||||
solana_system_interface::instruction::SystemInstruction::AssignWithSeed {
|
||||
base: address,
|
||||
seed: "seed".to_string(),
|
||||
owner: address,
|
||||
},
|
||||
2,
|
||||
"assign_with_seed",
|
||||
),
|
||||
(
|
||||
solana_system_interface::instruction::SystemInstruction::TransferWithSeed {
|
||||
lamports: 1,
|
||||
from_seed: "seed".to_string(),
|
||||
from_owner: address,
|
||||
},
|
||||
3,
|
||||
"transfer_with_seed",
|
||||
),
|
||||
(
|
||||
solana_system_interface::instruction::SystemInstruction::UpgradeNonceAccount,
|
||||
1,
|
||||
"upgrade_nonce_account",
|
||||
),
|
||||
(
|
||||
solana_system_interface::instruction::SystemInstruction::CreateAccountAllowPrefund {
|
||||
lamports: 1,
|
||||
space: 2,
|
||||
owner: address,
|
||||
},
|
||||
2,
|
||||
"create_account_allow_prefund",
|
||||
),
|
||||
];
|
||||
for (instruction, account_count, expected) in fixtures {
|
||||
let input = replay_input(&instruction, account_count, false);
|
||||
let result = crate::solana_core_system_decode(&input);
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
assert_eq!(
|
||||
result.recognized_entry_code.as_deref(),
|
||||
std::option::Option::Some(expected)
|
||||
);
|
||||
assert_eq!(result.observations.len(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn additional_accounts_are_preserved_without_rejecting_valid_system_instructions() {
|
||||
let instruction =
|
||||
solana_system_interface::instruction::SystemInstruction::Transfer { lamports: 1 };
|
||||
let result = crate::solana_core_system_decode(&replay_input(&instruction, 3, false));
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
let accounts = match result.observations[0].payload_json["accounts"].as_array() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => panic!("decoded System accounts are not an array"),
|
||||
};
|
||||
assert_eq!(accounts.len(), 3);
|
||||
assert_eq!(accounts[2]["role"], "additional_account");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_accounts_and_truncated_payload_fail_without_panic() {
|
||||
let instruction =
|
||||
solana_system_interface::instruction::SystemInstruction::Transfer { lamports: 1 };
|
||||
let missing = crate::solana_core_system_decode(&replay_input(&instruction, 1, false));
|
||||
assert_eq!(missing.status, crate::DecoderOutcomeStatus::Failed);
|
||||
let truncated = crate::solana_core_system_decode(&replay_input_bytes(&[2, 0, 0], 2, false));
|
||||
assert_eq!(truncated.status, crate::DecoderOutcomeStatus::Failed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_tag_is_unsupported_and_hashable_for_replay() {
|
||||
let result =
|
||||
crate::solana_core_system_decode(&replay_input_bytes(&[255, 0, 0, 0], 0, false));
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Unsupported);
|
||||
assert_eq!(
|
||||
result.recognized_entry_code.as_deref(),
|
||||
std::option::Option::Some("unknown_system_instruction")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_transaction_is_decoded_as_uncommitted_intent() {
|
||||
let instruction =
|
||||
solana_system_interface::instruction::SystemInstruction::Transfer { lamports: 55 };
|
||||
let result = crate::solana_core_system_decode(&replay_input(&instruction, 2, true));
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
assert!(!result.observations[0].observation_committed);
|
||||
assert_eq!(result.observations[0].payload_json["transactionSucceeded"], false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn event_serialization_is_deterministic() {
|
||||
let instruction =
|
||||
solana_system_interface::instruction::SystemInstruction::Transfer { lamports: 55 };
|
||||
let input = replay_input(&instruction, 2, false);
|
||||
let first = crate::solana_core_system_decode(&input);
|
||||
let second = crate::solana_core_system_decode(&input);
|
||||
let first_json = match serde_json::to_string(&first) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("first event serialization failed: {error}"),
|
||||
};
|
||||
let second_json = match serde_json::to_string(&second) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("second event serialization failed: {error}"),
|
||||
};
|
||||
assert_eq!(first_json, second_json);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coverage_declares_all_current_system_variants() {
|
||||
let coverage = crate::solana_core_system_coverage();
|
||||
assert_eq!(coverage.len(), 14);
|
||||
assert!(coverage.iter().any(|entry| {
|
||||
return entry.entry_code == "create_account_allow_prefund"
|
||||
&& entry.discriminator_hex.as_deref() == std::option::Option::Some("0d000000");
|
||||
}));
|
||||
}
|
||||
}
|
||||
1068
kb-lib/src/decoder/solana/core/vote.rs
Normal file
1068
kb-lib/src/decoder/solana/core/vote.rs
Normal file
File diff suppressed because it is too large
Load Diff
721
kb-lib/src/decoder/solana/core/zk_elgamal.rs
Normal file
721
kb-lib/src/decoder/solana/core/zk_elgamal.rs
Normal file
@@ -0,0 +1,721 @@
|
||||
// file: kb-lib/src/decoder/solana/core/zk_elgamal.rs
|
||||
// version: 4
|
||||
|
||||
//! Exact structural decoding for the native ZK ElGamal Proof program.
|
||||
|
||||
const SOURCE: &str = "solana-zk-elgamal-proof-interface@^0.1 ProofInstruction and Pod layouts; Agave native runtime proof-account mode";
|
||||
const PROOF_ACCOUNT_INSTRUCTION_BYTES: usize = 5;
|
||||
const PROOF_COMPONENT_PREFIX_BYTES: usize = 16;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct ProofDescriptor {
|
||||
entry_code: &'static str,
|
||||
proof_data_size: usize,
|
||||
context_data_size: usize,
|
||||
}
|
||||
|
||||
/// Returns declared ZK ElGamal Proof instruction coverage.
|
||||
pub(crate) fn zk_elgamal_coverage() -> std::vec::Vec<crate::DecoderCoverageDeclaration> {
|
||||
return instruction_entries()
|
||||
.iter()
|
||||
.map(|(tag, entry_code)| {
|
||||
return crate::DecoderCoverageDeclaration {
|
||||
program_id: kb_program_ids::ZK_ELGAMAL_PROOF_PROGRAM_ID.to_string(),
|
||||
surface_code: std::option::Option::Some(
|
||||
crate::SOLANA_CORE_ZK_ELGAMAL_PROOF_SURFACE_CODE.to_string(),
|
||||
),
|
||||
entry_kind: crate::DecoderCoverageEntryKind::Instruction,
|
||||
entry_code: (*entry_code).to_string(),
|
||||
discriminator_hex: std::option::Option::Some(format!("{tag:02x}")),
|
||||
historical: false,
|
||||
};
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
/// Recognizes one native ZK ElGamal Proof instruction.
|
||||
pub(crate) fn zk_elgamal_recognize(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
priority: u16,
|
||||
) -> crate::DecoderRecognition {
|
||||
let bytes = match crate::solana_core_decode_instruction_data(input) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_error) => {
|
||||
return crate::DecoderRecognition::compatible(
|
||||
false,
|
||||
priority,
|
||||
std::option::Option::Some(
|
||||
crate::SOLANA_CORE_ZK_ELGAMAL_PROOF_SURFACE_CODE.to_string(),
|
||||
),
|
||||
std::option::Option::Some("malformed_zk_elgamal_proof_instruction".to_string()),
|
||||
std::option::Option::None,
|
||||
);
|
||||
},
|
||||
};
|
||||
let instruction =
|
||||
match solana_zk_elgamal_proof_interface::instruction::ProofInstruction::instruction_type(
|
||||
bytes.as_slice(),
|
||||
) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
let entry_code = if bytes.is_empty() {
|
||||
"malformed_zk_elgamal_proof_instruction"
|
||||
} else {
|
||||
"unknown_zk_elgamal_proof_instruction"
|
||||
};
|
||||
return crate::DecoderRecognition::compatible(
|
||||
false,
|
||||
priority,
|
||||
std::option::Option::Some(
|
||||
crate::SOLANA_CORE_ZK_ELGAMAL_PROOF_SURFACE_CODE.to_string(),
|
||||
),
|
||||
std::option::Option::Some(entry_code.to_string()),
|
||||
crate::solana_core_hexadecimal_prefix(bytes.as_slice(), bytes.len().min(1)),
|
||||
);
|
||||
},
|
||||
};
|
||||
let (entry_code, exact) = match instruction {
|
||||
solana_zk_elgamal_proof_interface::instruction::ProofInstruction::CloseContextState => {
|
||||
("close_context_state", true)
|
||||
},
|
||||
_ => {
|
||||
let descriptor = proof_descriptor(instruction);
|
||||
let descriptor = match descriptor {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return crate::DecoderRecognition::compatible(
|
||||
false,
|
||||
priority,
|
||||
std::option::Option::Some(
|
||||
crate::SOLANA_CORE_ZK_ELGAMAL_PROOF_SURFACE_CODE.to_string(),
|
||||
),
|
||||
std::option::Option::Some(
|
||||
"unknown_zk_elgamal_proof_instruction".to_string(),
|
||||
),
|
||||
crate::solana_core_hexadecimal_prefix(bytes.as_slice(), 1),
|
||||
);
|
||||
},
|
||||
};
|
||||
let inline_size = descriptor.proof_data_size.saturating_add(1);
|
||||
(
|
||||
descriptor.entry_code,
|
||||
bytes.len() == PROOF_ACCOUNT_INSTRUCTION_BYTES || bytes.len() == inline_size,
|
||||
)
|
||||
},
|
||||
};
|
||||
return crate::DecoderRecognition::compatible(
|
||||
exact,
|
||||
priority,
|
||||
std::option::Option::Some(crate::SOLANA_CORE_ZK_ELGAMAL_PROOF_SURFACE_CODE.to_string()),
|
||||
std::option::Option::Some(entry_code.to_string()),
|
||||
crate::solana_core_hexadecimal_prefix(bytes.as_slice(), 1),
|
||||
);
|
||||
}
|
||||
|
||||
/// Decodes one native ZK ElGamal Proof instruction without recomputing the proof.
|
||||
pub(crate) fn zk_elgamal_decode(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
) -> crate::DecoderExecutionResult {
|
||||
let bytes = match crate::solana_core_decode_instruction_data(input) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some("malformed_zk_elgamal_proof_instruction"),
|
||||
"zk_elgamal_payload_invalid",
|
||||
error.to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
let instruction =
|
||||
match solana_zk_elgamal_proof_interface::instruction::ProofInstruction::instruction_type(
|
||||
bytes.as_slice(),
|
||||
) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
if bytes.is_empty() {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some("malformed_zk_elgamal_proof_instruction"),
|
||||
"zk_elgamal_payload_empty",
|
||||
"ZK ElGamal Proof instruction payload is empty",
|
||||
);
|
||||
}
|
||||
let tag = match bytes.first() {
|
||||
std::option::Option::Some(value) => *value,
|
||||
std::option::Option::None => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some("malformed_zk_elgamal_proof_instruction"),
|
||||
"zk_elgamal_payload_empty",
|
||||
"ZK ElGamal Proof instruction payload is empty",
|
||||
);
|
||||
},
|
||||
};
|
||||
return crate::solana_core_unsupported_result(
|
||||
"unknown_zk_elgamal_proof_instruction",
|
||||
"zk_elgamal_tag_unknown",
|
||||
format!(
|
||||
"unknown ZK ElGamal Proof instruction tag {tag}; payload_sha256={}",
|
||||
crate::solana_core_payload_hash(input)
|
||||
),
|
||||
);
|
||||
},
|
||||
};
|
||||
if instruction
|
||||
== solana_zk_elgamal_proof_interface::instruction::ProofInstruction::CloseContextState
|
||||
{
|
||||
return decode_close_context_state(input, bytes.as_slice());
|
||||
}
|
||||
let descriptor = match proof_descriptor(instruction) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return crate::solana_core_unsupported_result(
|
||||
"unknown_zk_elgamal_proof_instruction",
|
||||
"zk_elgamal_instruction_unmapped",
|
||||
"official ZK ElGamal Proof instruction has no local descriptor",
|
||||
);
|
||||
},
|
||||
};
|
||||
return decode_verify_proof(input, bytes.as_slice(), descriptor);
|
||||
}
|
||||
|
||||
fn decode_close_context_state(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
bytes: &[u8],
|
||||
) -> crate::DecoderExecutionResult {
|
||||
let roles = [
|
||||
crate::SolanaCoreAccountRole::new("proof_context_state", true, false),
|
||||
crate::SolanaCoreAccountRole::new("lamport_destination", true, false),
|
||||
crate::SolanaCoreAccountRole::new("context_state_authority", false, true),
|
||||
];
|
||||
let accounts = match crate::solana_core_resolve_accounts(
|
||||
input,
|
||||
roles.as_slice(),
|
||||
3,
|
||||
std::option::Option::None,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some("close_context_state"),
|
||||
"zk_elgamal_close_accounts_invalid",
|
||||
error.to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
let account_array = match accounts.as_array() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some("close_context_state"),
|
||||
"zk_elgamal_close_accounts_not_array",
|
||||
"resolved ZK ElGamal close accounts are not an array",
|
||||
);
|
||||
},
|
||||
};
|
||||
let context_key = account_array
|
||||
.first()
|
||||
.and_then(|value| return value.get("accountKey"))
|
||||
.and_then(serde_json::Value::as_str);
|
||||
let destination_key = account_array
|
||||
.get(1)
|
||||
.and_then(|value| return value.get("accountKey"))
|
||||
.and_then(serde_json::Value::as_str);
|
||||
if context_key.is_some() && context_key == destination_key {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some("close_context_state"),
|
||||
"zk_elgamal_close_destination_matches_context",
|
||||
"ZK ElGamal proof context account and lamport destination must differ",
|
||||
);
|
||||
}
|
||||
let trailing = match bytes.get(1..) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some("close_context_state"),
|
||||
"zk_elgamal_close_payload_missing_tag",
|
||||
"ZK ElGamal close-context instruction has no discriminator byte",
|
||||
);
|
||||
},
|
||||
};
|
||||
return crate::solana_core_decoded_result(
|
||||
input,
|
||||
crate::SOLANA_CORE_ZK_ELGAMAL_PROOF_SURFACE_CODE,
|
||||
"close_context_state",
|
||||
crate::EventFamily::Lifecycle,
|
||||
false,
|
||||
accounts,
|
||||
serde_json::json!({
|
||||
"instructionMode": "close_context_state",
|
||||
"runtimeAcceptsTrailingBytes": true,
|
||||
"trailingByteLength": trailing.len(),
|
||||
"trailingSha256": crate::solana_core_hash_bytes(trailing),
|
||||
"trailingPrefixHex": crate::solana_core_bounded_hexadecimal_prefix(
|
||||
trailing,
|
||||
PROOF_COMPONENT_PREFIX_BYTES,
|
||||
),
|
||||
"reclaimsLamports": true,
|
||||
"resetsOwnerToSystemProgram": true,
|
||||
"runtimeVerification": runtime_verification(input),
|
||||
}),
|
||||
SOURCE,
|
||||
);
|
||||
}
|
||||
|
||||
fn decode_verify_proof(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
bytes: &[u8],
|
||||
descriptor: ProofDescriptor,
|
||||
) -> crate::DecoderExecutionResult {
|
||||
let external_proof = bytes.len() == PROOF_ACCOUNT_INSTRUCTION_BYTES;
|
||||
let expected_inline_size = descriptor.proof_data_size.saturating_add(1);
|
||||
if !external_proof && bytes.len() != expected_inline_size {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some(descriptor.entry_code),
|
||||
"zk_elgamal_proof_data_size_invalid",
|
||||
format!(
|
||||
"{} requires either {} bytes for proof-account mode or {} bytes for inline proof data but received {}",
|
||||
descriptor.entry_code,
|
||||
PROOF_ACCOUNT_INSTRUCTION_BYTES,
|
||||
expected_inline_size,
|
||||
bytes.len()
|
||||
),
|
||||
);
|
||||
}
|
||||
let account_count = match input.instruction_accounts_json.as_array() {
|
||||
std::option::Option::Some(value) => value.len(),
|
||||
std::option::Option::None => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some(descriptor.entry_code),
|
||||
"zk_elgamal_accounts_not_array",
|
||||
"ZK ElGamal Proof instruction accounts are not an array",
|
||||
);
|
||||
},
|
||||
};
|
||||
let context_state_requested =
|
||||
if external_proof { account_count >= 3 } else { account_count >= 2 };
|
||||
let mut roles = std::vec::Vec::new();
|
||||
if external_proof {
|
||||
roles.push(crate::SolanaCoreAccountRole::new("proof_data_account", false, false));
|
||||
}
|
||||
if context_state_requested {
|
||||
roles.push(crate::SolanaCoreAccountRole::new("proof_context_state", true, false));
|
||||
roles.push(crate::SolanaCoreAccountRole::new("context_state_authority", false, false));
|
||||
}
|
||||
let minimum_accounts = if external_proof { 1 } else { 0 };
|
||||
let accounts = match crate::solana_core_resolve_accounts(
|
||||
input,
|
||||
roles.as_slice(),
|
||||
minimum_accounts,
|
||||
std::option::Option::None,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some(descriptor.entry_code),
|
||||
"zk_elgamal_accounts_invalid",
|
||||
error.to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
let proof_parameters = if external_proof {
|
||||
let offset = match crate::solana_core_read_u32_le(bytes, 1) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some(descriptor.entry_code),
|
||||
"zk_elgamal_proof_account_offset_invalid",
|
||||
error.to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
serde_json::json!({
|
||||
"proofDataSource": "account",
|
||||
"proofAccountOffset": offset,
|
||||
"proofDataAvailability": "external_account_not_captured_by_transaction_core",
|
||||
"proofDataByteLength": descriptor.proof_data_size,
|
||||
"contextDataByteLength": descriptor.context_data_size,
|
||||
"proofBodyByteLength": descriptor.proof_data_size.saturating_sub(descriptor.context_data_size),
|
||||
"proofDataSha256": serde_json::Value::Null,
|
||||
"contextDataSha256": serde_json::Value::Null,
|
||||
"proofBodySha256": serde_json::Value::Null,
|
||||
})
|
||||
} else {
|
||||
let proof_data = match bytes.get(1..) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some(descriptor.entry_code),
|
||||
"zk_elgamal_inline_proof_missing",
|
||||
"inline ZK ElGamal proof data is missing",
|
||||
);
|
||||
},
|
||||
};
|
||||
let context_data = match proof_data.get(..descriptor.context_data_size) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some(descriptor.entry_code),
|
||||
"zk_elgamal_inline_context_truncated",
|
||||
"inline ZK ElGamal proof context data is truncated",
|
||||
);
|
||||
},
|
||||
};
|
||||
let proof_body = match proof_data.get(descriptor.context_data_size..) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some(descriptor.entry_code),
|
||||
"zk_elgamal_inline_proof_body_truncated",
|
||||
"inline ZK ElGamal proof body is truncated",
|
||||
);
|
||||
},
|
||||
};
|
||||
serde_json::json!({
|
||||
"proofDataSource": "instruction_data",
|
||||
"proofAccountOffset": serde_json::Value::Null,
|
||||
"proofDataAvailability": "inline_hashed_not_retained",
|
||||
"proofDataByteLength": proof_data.len(),
|
||||
"contextDataByteLength": context_data.len(),
|
||||
"proofBodyByteLength": proof_body.len(),
|
||||
"proofDataSha256": crate::solana_core_hash_bytes(proof_data),
|
||||
"proofDataPrefixHex": crate::solana_core_bounded_hexadecimal_prefix(
|
||||
proof_data,
|
||||
PROOF_COMPONENT_PREFIX_BYTES,
|
||||
),
|
||||
"contextDataSha256": crate::solana_core_hash_bytes(context_data),
|
||||
"contextDataPrefixHex": crate::solana_core_bounded_hexadecimal_prefix(
|
||||
context_data,
|
||||
PROOF_COMPONENT_PREFIX_BYTES,
|
||||
),
|
||||
"proofBodySha256": crate::solana_core_hash_bytes(proof_body),
|
||||
"proofBodyPrefixHex": crate::solana_core_bounded_hexadecimal_prefix(
|
||||
proof_body,
|
||||
PROOF_COMPONENT_PREFIX_BYTES,
|
||||
),
|
||||
})
|
||||
};
|
||||
return crate::solana_core_decoded_result(
|
||||
input,
|
||||
crate::SOLANA_CORE_ZK_ELGAMAL_PROOF_SURFACE_CODE,
|
||||
descriptor.entry_code,
|
||||
crate::EventFamily::Audit,
|
||||
false,
|
||||
accounts,
|
||||
serde_json::json!({
|
||||
"proofType": descriptor.entry_code,
|
||||
"proof": proof_parameters,
|
||||
"contextStateRequested": context_state_requested,
|
||||
"contextStateMutation": if context_state_requested {
|
||||
"initialize_after_successful_verification"
|
||||
} else {
|
||||
"none"
|
||||
},
|
||||
"cryptographicVerificationPerformedByDecoder": false,
|
||||
"runtimeVerification": runtime_verification(input),
|
||||
}),
|
||||
SOURCE,
|
||||
);
|
||||
}
|
||||
|
||||
fn proof_descriptor(
|
||||
instruction: solana_zk_elgamal_proof_interface::instruction::ProofInstruction,
|
||||
) -> std::option::Option<ProofDescriptor> {
|
||||
let descriptor = match instruction {
|
||||
solana_zk_elgamal_proof_interface::instruction::ProofInstruction::CloseContextState => {
|
||||
return std::option::Option::None;
|
||||
},
|
||||
solana_zk_elgamal_proof_interface::instruction::ProofInstruction::VerifyZeroCiphertext => {
|
||||
ProofDescriptor {
|
||||
entry_code: "verify_zero_ciphertext",
|
||||
proof_data_size: std::mem::size_of::<solana_zk_elgamal_proof_interface::proof_data::ZeroCiphertextProofData>(),
|
||||
context_data_size: std::mem::size_of::<solana_zk_elgamal_proof_interface::proof_data::ZeroCiphertextProofContext>(),
|
||||
}
|
||||
},
|
||||
solana_zk_elgamal_proof_interface::instruction::ProofInstruction::VerifyCiphertextCiphertextEquality => ProofDescriptor {
|
||||
entry_code: "verify_ciphertext_ciphertext_equality",
|
||||
proof_data_size: std::mem::size_of::<solana_zk_elgamal_proof_interface::proof_data::CiphertextCiphertextEqualityProofData>(),
|
||||
context_data_size: std::mem::size_of::<solana_zk_elgamal_proof_interface::proof_data::CiphertextCiphertextEqualityProofContext>(),
|
||||
},
|
||||
solana_zk_elgamal_proof_interface::instruction::ProofInstruction::VerifyCiphertextCommitmentEquality => ProofDescriptor {
|
||||
entry_code: "verify_ciphertext_commitment_equality",
|
||||
proof_data_size: std::mem::size_of::<solana_zk_elgamal_proof_interface::proof_data::CiphertextCommitmentEqualityProofData>(),
|
||||
context_data_size: std::mem::size_of::<solana_zk_elgamal_proof_interface::proof_data::CiphertextCommitmentEqualityProofContext>(),
|
||||
},
|
||||
solana_zk_elgamal_proof_interface::instruction::ProofInstruction::VerifyPubkeyValidity => {
|
||||
ProofDescriptor {
|
||||
entry_code: "verify_pubkey_validity",
|
||||
proof_data_size: std::mem::size_of::<solana_zk_elgamal_proof_interface::proof_data::PubkeyValidityProofData>(),
|
||||
context_data_size: std::mem::size_of::<solana_zk_elgamal_proof_interface::proof_data::PubkeyValidityProofContext>(),
|
||||
}
|
||||
},
|
||||
solana_zk_elgamal_proof_interface::instruction::ProofInstruction::VerifyPercentageWithCap => ProofDescriptor {
|
||||
entry_code: "verify_percentage_with_cap",
|
||||
proof_data_size: std::mem::size_of::<solana_zk_elgamal_proof_interface::proof_data::PercentageWithCapProofData>(),
|
||||
context_data_size: std::mem::size_of::<solana_zk_elgamal_proof_interface::proof_data::PercentageWithCapProofContext>(),
|
||||
},
|
||||
solana_zk_elgamal_proof_interface::instruction::ProofInstruction::VerifyBatchedRangeProofU64 => ProofDescriptor {
|
||||
entry_code: "verify_batched_range_proof_u64",
|
||||
proof_data_size: std::mem::size_of::<solana_zk_elgamal_proof_interface::proof_data::BatchedRangeProofU64Data>(),
|
||||
context_data_size: std::mem::size_of::<solana_zk_elgamal_proof_interface::proof_data::BatchedRangeProofContext>(),
|
||||
},
|
||||
solana_zk_elgamal_proof_interface::instruction::ProofInstruction::VerifyBatchedRangeProofU128 => ProofDescriptor {
|
||||
entry_code: "verify_batched_range_proof_u128",
|
||||
proof_data_size: std::mem::size_of::<solana_zk_elgamal_proof_interface::proof_data::BatchedRangeProofU128Data>(),
|
||||
context_data_size: std::mem::size_of::<solana_zk_elgamal_proof_interface::proof_data::BatchedRangeProofContext>(),
|
||||
},
|
||||
solana_zk_elgamal_proof_interface::instruction::ProofInstruction::VerifyBatchedRangeProofU256 => ProofDescriptor {
|
||||
entry_code: "verify_batched_range_proof_u256",
|
||||
proof_data_size: std::mem::size_of::<solana_zk_elgamal_proof_interface::proof_data::BatchedRangeProofU256Data>(),
|
||||
context_data_size: std::mem::size_of::<solana_zk_elgamal_proof_interface::proof_data::BatchedRangeProofContext>(),
|
||||
},
|
||||
solana_zk_elgamal_proof_interface::instruction::ProofInstruction::VerifyGroupedCiphertext2HandlesValidity => ProofDescriptor {
|
||||
entry_code: "verify_grouped_ciphertext_2_handles_validity",
|
||||
proof_data_size: std::mem::size_of::<solana_zk_elgamal_proof_interface::proof_data::GroupedCiphertext2HandlesValidityProofData>(),
|
||||
context_data_size: std::mem::size_of::<solana_zk_elgamal_proof_interface::proof_data::GroupedCiphertext2HandlesValidityProofContext>(),
|
||||
},
|
||||
solana_zk_elgamal_proof_interface::instruction::ProofInstruction::VerifyBatchedGroupedCiphertext2HandlesValidity => ProofDescriptor {
|
||||
entry_code: "verify_batched_grouped_ciphertext_2_handles_validity",
|
||||
proof_data_size: std::mem::size_of::<solana_zk_elgamal_proof_interface::proof_data::BatchedGroupedCiphertext2HandlesValidityProofData>(),
|
||||
context_data_size: std::mem::size_of::<solana_zk_elgamal_proof_interface::proof_data::BatchedGroupedCiphertext2HandlesValidityProofContext>(),
|
||||
},
|
||||
solana_zk_elgamal_proof_interface::instruction::ProofInstruction::VerifyGroupedCiphertext3HandlesValidity => ProofDescriptor {
|
||||
entry_code: "verify_grouped_ciphertext_3_handles_validity",
|
||||
proof_data_size: std::mem::size_of::<solana_zk_elgamal_proof_interface::proof_data::GroupedCiphertext3HandlesValidityProofData>(),
|
||||
context_data_size: std::mem::size_of::<solana_zk_elgamal_proof_interface::proof_data::GroupedCiphertext3HandlesValidityProofContext>(),
|
||||
},
|
||||
solana_zk_elgamal_proof_interface::instruction::ProofInstruction::VerifyBatchedGroupedCiphertext3HandlesValidity => ProofDescriptor {
|
||||
entry_code: "verify_batched_grouped_ciphertext_3_handles_validity",
|
||||
proof_data_size: std::mem::size_of::<solana_zk_elgamal_proof_interface::proof_data::BatchedGroupedCiphertext3HandlesValidityProofData>(),
|
||||
context_data_size: std::mem::size_of::<solana_zk_elgamal_proof_interface::proof_data::BatchedGroupedCiphertext3HandlesValidityProofContext>(),
|
||||
},
|
||||
};
|
||||
return std::option::Option::Some(descriptor);
|
||||
}
|
||||
|
||||
fn runtime_verification(input: &crate::CoreInstructionReplayInput) -> &'static str {
|
||||
if input.transaction_failed {
|
||||
return "not_asserted_transaction_failed";
|
||||
}
|
||||
return "accepted_in_successful_transaction";
|
||||
}
|
||||
|
||||
fn instruction_entries() -> &'static [(u8, &'static str)] {
|
||||
return &[
|
||||
(0, "close_context_state"),
|
||||
(1, "verify_zero_ciphertext"),
|
||||
(2, "verify_ciphertext_ciphertext_equality"),
|
||||
(3, "verify_ciphertext_commitment_equality"),
|
||||
(4, "verify_pubkey_validity"),
|
||||
(5, "verify_percentage_with_cap"),
|
||||
(6, "verify_batched_range_proof_u64"),
|
||||
(7, "verify_batched_range_proof_u128"),
|
||||
(8, "verify_batched_range_proof_u256"),
|
||||
(9, "verify_grouped_ciphertext_2_handles_validity"),
|
||||
(10, "verify_batched_grouped_ciphertext_2_handles_validity"),
|
||||
(11, "verify_grouped_ciphertext_3_handles_validity"),
|
||||
(12, "verify_batched_grouped_ciphertext_3_handles_validity"),
|
||||
];
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use base64::Engine; // rust-rules: trait-import
|
||||
|
||||
fn replay_input(
|
||||
bytes: &[u8],
|
||||
account_count: usize,
|
||||
transaction_failed: bool,
|
||||
) -> crate::CoreInstructionReplayInput {
|
||||
let account_keys = (0..account_count)
|
||||
.map(|index| {
|
||||
return serde_json::json!({
|
||||
"accountIndex": index,
|
||||
"accountKey": format!("ZkAccount{index:02}111111111111111111111111111"),
|
||||
"source": "static",
|
||||
"writable": true,
|
||||
"signer": index == 2,
|
||||
"executable": false,
|
||||
});
|
||||
})
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
let instruction_accounts = account_keys
|
||||
.iter()
|
||||
.map(|account| {
|
||||
return serde_json::json!({
|
||||
"accountIndex": account["accountIndex"],
|
||||
"accountKey": account["accountKey"],
|
||||
});
|
||||
})
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
let result = crate::CoreInstructionReplayInput::new(
|
||||
"signature:0",
|
||||
"signature",
|
||||
42,
|
||||
"0",
|
||||
kb_program_ids::ZK_ELGAMAL_PROOF_PROGRAM_ID,
|
||||
transaction_failed,
|
||||
if transaction_failed {
|
||||
std::option::Option::Some(serde_json::json!({"InstructionError": [0, "Custom"]}))
|
||||
} else {
|
||||
std::option::Option::None
|
||||
},
|
||||
serde_json::Value::Array(account_keys),
|
||||
serde_json::Value::Array(instruction_accounts),
|
||||
std::option::Option::Some(serde_json::json!({
|
||||
"dataBase64": base64::engine::general_purpose::STANDARD.encode(bytes),
|
||||
})),
|
||||
std::option::Option::Some("payload-hash".to_string()),
|
||||
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!("ZK ElGamal replay input failed: {error}"),
|
||||
};
|
||||
}
|
||||
|
||||
fn inline_fixture(
|
||||
tag: u8,
|
||||
instruction: solana_zk_elgamal_proof_interface::instruction::ProofInstruction,
|
||||
) -> std::vec::Vec<u8> {
|
||||
let descriptor = match super::proof_descriptor(instruction) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => panic!("proof descriptor missing"),
|
||||
};
|
||||
let mut bytes = vec![tag];
|
||||
bytes.resize(descriptor.proof_data_size.saturating_add(1), tag);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coverage_declares_all_thirteen_official_instructions() {
|
||||
let coverage = crate::solana_core_zk_elgamal_coverage();
|
||||
assert_eq!(coverage.len(), 13);
|
||||
for (tag, entry_code) in super::instruction_entries() {
|
||||
let expected_discriminator = format!("{tag:02x}");
|
||||
assert!(coverage.iter().any(|entry| {
|
||||
return entry.entry_code == *entry_code
|
||||
&& entry.discriminator_hex.as_deref()
|
||||
== std::option::Option::Some(expected_discriminator.as_str());
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn official_discriminants_map_to_stable_entry_codes() {
|
||||
for (tag, entry_code) in super::instruction_entries() {
|
||||
let recognition =
|
||||
crate::solana_core_zk_elgamal_recognize(&replay_input(&[*tag], 3, false), 100);
|
||||
assert!(recognition.compatible);
|
||||
assert_eq!(recognition.entry_code.as_deref(), std::option::Option::Some(*entry_code));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proof_account_mode_decodes_offset_and_context_request() {
|
||||
let bytes = [1_u8, 44, 0, 0, 0];
|
||||
let result = crate::solana_core_zk_elgamal_decode(&replay_input(&bytes, 3, false));
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
let parameters = &result.observations[0].payload_json["parameters"];
|
||||
assert_eq!(parameters["proof"]["proofDataSource"], "account");
|
||||
assert_eq!(parameters["proof"]["proofAccountOffset"], 44);
|
||||
assert_eq!(parameters["contextStateRequested"], true);
|
||||
assert_eq!(parameters["cryptographicVerificationPerformedByDecoder"], false);
|
||||
assert_eq!(parameters["runtimeVerification"], "accepted_in_successful_transaction");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proof_account_mode_requires_the_proof_account() {
|
||||
let result =
|
||||
crate::solana_core_zk_elgamal_decode(&replay_input(&[1, 0, 0, 0, 0], 0, false));
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Failed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_inline_proof_layout_is_bounded_by_official_pod_sizes() {
|
||||
let variants = [
|
||||
(1, solana_zk_elgamal_proof_interface::instruction::ProofInstruction::VerifyZeroCiphertext),
|
||||
(2, solana_zk_elgamal_proof_interface::instruction::ProofInstruction::VerifyCiphertextCiphertextEquality),
|
||||
(3, solana_zk_elgamal_proof_interface::instruction::ProofInstruction::VerifyCiphertextCommitmentEquality),
|
||||
(4, solana_zk_elgamal_proof_interface::instruction::ProofInstruction::VerifyPubkeyValidity),
|
||||
(5, solana_zk_elgamal_proof_interface::instruction::ProofInstruction::VerifyPercentageWithCap),
|
||||
(6, solana_zk_elgamal_proof_interface::instruction::ProofInstruction::VerifyBatchedRangeProofU64),
|
||||
(7, solana_zk_elgamal_proof_interface::instruction::ProofInstruction::VerifyBatchedRangeProofU128),
|
||||
(8, solana_zk_elgamal_proof_interface::instruction::ProofInstruction::VerifyBatchedRangeProofU256),
|
||||
(9, solana_zk_elgamal_proof_interface::instruction::ProofInstruction::VerifyGroupedCiphertext2HandlesValidity),
|
||||
(10, solana_zk_elgamal_proof_interface::instruction::ProofInstruction::VerifyBatchedGroupedCiphertext2HandlesValidity),
|
||||
(11, solana_zk_elgamal_proof_interface::instruction::ProofInstruction::VerifyGroupedCiphertext3HandlesValidity),
|
||||
(12, solana_zk_elgamal_proof_interface::instruction::ProofInstruction::VerifyBatchedGroupedCiphertext3HandlesValidity),
|
||||
];
|
||||
for (tag, instruction) in variants {
|
||||
let bytes = inline_fixture(tag, instruction);
|
||||
let result =
|
||||
crate::solana_core_zk_elgamal_decode(&replay_input(bytes.as_slice(), 0, true));
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
assert_eq!(result.observations.len(), 1);
|
||||
assert!(!result.observations[0].observation_committed);
|
||||
assert_eq!(
|
||||
result.observations[0].payload_json["parameters"]["proof"]["proofDataByteLength"],
|
||||
bytes.len().saturating_sub(1)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_inline_sizes_unknown_tags_and_empty_payload_fail_safely() {
|
||||
let truncated = crate::solana_core_zk_elgamal_decode(&replay_input(&[1, 2, 3], 0, false));
|
||||
assert_eq!(truncated.status, crate::DecoderOutcomeStatus::Failed);
|
||||
let unknown = crate::solana_core_zk_elgamal_decode(&replay_input(&[99], 0, false));
|
||||
assert_eq!(unknown.status, crate::DecoderOutcomeStatus::Unsupported);
|
||||
let empty = crate::solana_core_zk_elgamal_decode(&replay_input(&[], 0, false));
|
||||
assert_eq!(empty.status, crate::DecoderOutcomeStatus::Failed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn close_context_state_accepts_runtime_trailing_bytes_and_rejects_same_destination() {
|
||||
let decoded = crate::solana_core_zk_elgamal_decode(&replay_input(&[0, 7, 8], 3, false));
|
||||
assert_eq!(decoded.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
assert_eq!(decoded.observations[0].payload_json["parameters"]["trailingByteLength"], 2);
|
||||
let mut same = replay_input(&[0], 3, false);
|
||||
let accounts = match same.instruction_accounts_json.as_array_mut() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => panic!("instruction accounts missing"),
|
||||
};
|
||||
accounts[1]["accountKey"] = accounts[0]["accountKey"].clone();
|
||||
let keys = match same.account_keys_json.as_array_mut() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => panic!("account keys missing"),
|
||||
};
|
||||
keys[1]["accountKey"] = keys[0]["accountKey"].clone();
|
||||
let failed = crate::solana_core_zk_elgamal_decode(&same);
|
||||
assert_eq!(failed.status, crate::DecoderOutcomeStatus::Failed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_transaction_never_claims_runtime_verification_or_commit() {
|
||||
let result = crate::solana_core_zk_elgamal_decode(&replay_input(&[4, 0, 0, 0, 0], 1, true));
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
assert!(!result.observations[0].observation_committed);
|
||||
assert_eq!(
|
||||
result.observations[0].payload_json["parameters"]["runtimeVerification"],
|
||||
"not_asserted_transaction_failed"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialization_is_deterministic() {
|
||||
let input = replay_input(&[1, 0, 0, 0, 0], 1, false);
|
||||
let first = crate::solana_core_zk_elgamal_decode(&input);
|
||||
let second = crate::solana_core_zk_elgamal_decode(&input);
|
||||
let first_json = match serde_json::to_string(&first.observations[0].payload_json) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("first serialization failed: {error}"),
|
||||
};
|
||||
let second_json = match serde_json::to_string(&second.observations[0].payload_json) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("second serialization failed: {error}"),
|
||||
};
|
||||
assert_eq!(first_json, second_json);
|
||||
}
|
||||
}
|
||||
973
kb-lib/src/decoder/solana/core/zk_token_proof.rs
Normal file
973
kb-lib/src/decoder/solana/core/zk_token_proof.rs
Normal file
@@ -0,0 +1,973 @@
|
||||
// file: kb-lib/src/decoder/solana/core/zk_token_proof.rs
|
||||
// version: 4
|
||||
|
||||
//! Historical ZK Token Proof interface decoding with explicit current no-op runtime semantics.
|
||||
|
||||
const SOURCE: &str = "local bounded mirror of the historical ZK Token Proof wire contract audited against solana-zk-token-sdk 3.1.14 and Agave v2.0.0; Agave v4.1.1 no-op runtime stub";
|
||||
const PROOF_ACCOUNT_INSTRUCTION_BYTES: usize = 5;
|
||||
const PROOF_COMPONENT_PREFIX_BYTES: usize = 16;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
#[repr(u8)]
|
||||
enum HistoricalProofInstruction {
|
||||
CloseContextState = 0,
|
||||
VerifyZeroBalance = 1,
|
||||
VerifyWithdraw = 2,
|
||||
VerifyCiphertextCiphertextEquality = 3,
|
||||
VerifyTransfer = 4,
|
||||
VerifyTransferWithFee = 5,
|
||||
VerifyPubkeyValidity = 6,
|
||||
VerifyRangeProofU64 = 7,
|
||||
VerifyBatchedRangeProofU64 = 8,
|
||||
VerifyBatchedRangeProofU128 = 9,
|
||||
VerifyBatchedRangeProofU256 = 10,
|
||||
VerifyCiphertextCommitmentEquality = 11,
|
||||
VerifyGroupedCiphertext2HandlesValidity = 12,
|
||||
VerifyBatchedGroupedCiphertext2HandlesValidity = 13,
|
||||
VerifyFeeSigma = 14,
|
||||
VerifyGroupedCiphertext3HandlesValidity = 15,
|
||||
VerifyBatchedGroupedCiphertext3HandlesValidity = 16,
|
||||
}
|
||||
|
||||
impl HistoricalProofInstruction {
|
||||
fn from_tag(tag: u8) -> std::option::Option<Self> {
|
||||
return match tag {
|
||||
0 => std::option::Option::Some(Self::CloseContextState),
|
||||
1 => std::option::Option::Some(Self::VerifyZeroBalance),
|
||||
2 => std::option::Option::Some(Self::VerifyWithdraw),
|
||||
3 => std::option::Option::Some(Self::VerifyCiphertextCiphertextEquality),
|
||||
4 => std::option::Option::Some(Self::VerifyTransfer),
|
||||
5 => std::option::Option::Some(Self::VerifyTransferWithFee),
|
||||
6 => std::option::Option::Some(Self::VerifyPubkeyValidity),
|
||||
7 => std::option::Option::Some(Self::VerifyRangeProofU64),
|
||||
8 => std::option::Option::Some(Self::VerifyBatchedRangeProofU64),
|
||||
9 => std::option::Option::Some(Self::VerifyBatchedRangeProofU128),
|
||||
10 => std::option::Option::Some(Self::VerifyBatchedRangeProofU256),
|
||||
11 => std::option::Option::Some(Self::VerifyCiphertextCommitmentEquality),
|
||||
12 => std::option::Option::Some(Self::VerifyGroupedCiphertext2HandlesValidity),
|
||||
13 => std::option::Option::Some(Self::VerifyBatchedGroupedCiphertext2HandlesValidity),
|
||||
14 => std::option::Option::Some(Self::VerifyFeeSigma),
|
||||
15 => std::option::Option::Some(Self::VerifyGroupedCiphertext3HandlesValidity),
|
||||
16 => std::option::Option::Some(Self::VerifyBatchedGroupedCiphertext3HandlesValidity),
|
||||
_ => std::option::Option::None,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn historical_instruction(bytes: &[u8]) -> std::option::Option<HistoricalProofInstruction> {
|
||||
return bytes.first().copied().and_then(HistoricalProofInstruction::from_tag);
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct ProofDescriptor {
|
||||
entry_code: &'static str,
|
||||
proof_data_size: usize,
|
||||
context_data_size: usize,
|
||||
historical_inline_availability: &'static str,
|
||||
}
|
||||
|
||||
/// Returns declared historical and current ZK Token Proof coverage.
|
||||
pub(crate) fn zk_token_proof_coverage() -> std::vec::Vec<crate::DecoderCoverageDeclaration> {
|
||||
let mut coverage = instruction_entries()
|
||||
.iter()
|
||||
.map(|(tag, entry_code)| {
|
||||
return crate::DecoderCoverageDeclaration {
|
||||
program_id: kb_program_ids::ZK_TOKEN_PROOF_PROGRAM_ID.to_string(),
|
||||
surface_code: std::option::Option::Some(
|
||||
crate::SOLANA_CORE_ZK_TOKEN_PROOF_SURFACE_CODE.to_string(),
|
||||
),
|
||||
entry_kind: crate::DecoderCoverageEntryKind::Instruction,
|
||||
entry_code: (*entry_code).to_string(),
|
||||
discriminator_hex: std::option::Option::Some(format!("{tag:02x}")),
|
||||
historical: true,
|
||||
};
|
||||
})
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
coverage.push(crate::DecoderCoverageDeclaration {
|
||||
program_id: kb_program_ids::ZK_TOKEN_PROOF_PROGRAM_ID.to_string(),
|
||||
surface_code: std::option::Option::Some(
|
||||
crate::SOLANA_CORE_ZK_TOKEN_PROOF_SURFACE_CODE.to_string(),
|
||||
),
|
||||
entry_kind: crate::DecoderCoverageEntryKind::Instruction,
|
||||
entry_code: "current_runtime_noop_invocation".to_string(),
|
||||
discriminator_hex: std::option::Option::None,
|
||||
historical: false,
|
||||
});
|
||||
return coverage;
|
||||
}
|
||||
|
||||
/// Recognizes one historical ZK Token Proof layout or the current no-op runtime fallback.
|
||||
pub(crate) fn zk_token_proof_recognize(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
priority: u16,
|
||||
) -> crate::DecoderRecognition {
|
||||
let bytes = match crate::solana_core_decode_instruction_data(input) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_error) => {
|
||||
return crate::DecoderRecognition::compatible(
|
||||
false,
|
||||
priority,
|
||||
std::option::Option::Some(
|
||||
crate::SOLANA_CORE_ZK_TOKEN_PROOF_SURFACE_CODE.to_string(),
|
||||
),
|
||||
std::option::Option::Some("malformed_zk_token_proof_instruction".to_string()),
|
||||
std::option::Option::None,
|
||||
);
|
||||
},
|
||||
};
|
||||
let instruction = historical_instruction(bytes.as_slice());
|
||||
let entry_code = match instruction {
|
||||
std::option::Option::Some(HistoricalProofInstruction::CloseContextState) => {
|
||||
"close_context_state"
|
||||
},
|
||||
std::option::Option::Some(value) => {
|
||||
let descriptor = proof_descriptor(value);
|
||||
match descriptor {
|
||||
std::option::Option::Some(value) => {
|
||||
let inline_size = value.proof_data_size.checked_add(1);
|
||||
if bytes.len() == PROOF_ACCOUNT_INSTRUCTION_BYTES
|
||||
|| inline_size.is_some_and(|size| return bytes.len() == size)
|
||||
{
|
||||
value.entry_code
|
||||
} else {
|
||||
"current_runtime_noop_invocation"
|
||||
}
|
||||
},
|
||||
std::option::Option::None => "current_runtime_noop_invocation",
|
||||
}
|
||||
},
|
||||
std::option::Option::None => "current_runtime_noop_invocation",
|
||||
};
|
||||
return crate::DecoderRecognition::compatible(
|
||||
true,
|
||||
priority,
|
||||
std::option::Option::Some(crate::SOLANA_CORE_ZK_TOKEN_PROOF_SURFACE_CODE.to_string()),
|
||||
std::option::Option::Some(entry_code.to_string()),
|
||||
crate::solana_core_hexadecimal_prefix(bytes.as_slice(), bytes.len().min(1)),
|
||||
);
|
||||
}
|
||||
|
||||
/// Decodes one historical ZK Token Proof layout without recomputing any proof.
|
||||
pub(crate) fn zk_token_proof_decode(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
) -> crate::DecoderExecutionResult {
|
||||
let bytes = match crate::solana_core_decode_instruction_data(input) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some("malformed_zk_token_proof_instruction"),
|
||||
"zk_token_proof_payload_invalid",
|
||||
error.to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
let instruction = historical_instruction(bytes.as_slice());
|
||||
let instruction = match instruction {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return decode_current_noop(
|
||||
input,
|
||||
bytes.as_slice(),
|
||||
"empty_or_unknown_historical_discriminator",
|
||||
);
|
||||
},
|
||||
};
|
||||
if instruction == HistoricalProofInstruction::CloseContextState {
|
||||
return decode_close_context_state(input, bytes.as_slice());
|
||||
}
|
||||
let descriptor = match proof_descriptor(instruction) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return decode_current_noop(input, bytes.as_slice(), "unmapped_historical_instruction");
|
||||
},
|
||||
};
|
||||
let inline_size = match descriptor.proof_data_size.checked_add(1) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some(descriptor.entry_code),
|
||||
"zk_token_proof_inline_size_overflow",
|
||||
"historical ZK Token Proof inline size overflowed usize",
|
||||
);
|
||||
},
|
||||
};
|
||||
if bytes.len() != PROOF_ACCOUNT_INSTRUCTION_BYTES && bytes.len() != inline_size {
|
||||
return decode_current_noop(input, bytes.as_slice(), "historical_proof_size_mismatch");
|
||||
}
|
||||
return decode_verify_proof(input, bytes.as_slice(), descriptor);
|
||||
}
|
||||
|
||||
fn decode_current_noop(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
bytes: &[u8],
|
||||
historical_parse_status: &str,
|
||||
) -> crate::DecoderExecutionResult {
|
||||
let accounts =
|
||||
match crate::solana_core_resolve_accounts(input, &[], 0, std::option::Option::None) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some("current_runtime_noop_invocation"),
|
||||
"zk_token_proof_accounts_invalid",
|
||||
error.to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
return crate::solana_core_decoded_result(
|
||||
input,
|
||||
crate::SOLANA_CORE_ZK_TOKEN_PROOF_SURFACE_CODE,
|
||||
"current_runtime_noop_invocation",
|
||||
crate::EventFamily::Audit,
|
||||
false,
|
||||
accounts,
|
||||
serde_json::json!({
|
||||
"instructionMode": "opaque_current_runtime_noop",
|
||||
"historicalParseStatus": historical_parse_status,
|
||||
"historicalDiscriminator": bytes.first().copied(),
|
||||
"payloadByteLength": bytes.len(),
|
||||
"payloadSha256": crate::solana_core_hash_bytes(bytes),
|
||||
"payloadPrefixHex": crate::solana_core_bounded_hexadecimal_prefix(
|
||||
bytes,
|
||||
PROOF_COMPONENT_PREFIX_BYTES,
|
||||
),
|
||||
"historicalActivationEvidence": "no_public_activation_epoch_documented_in_feature_gate_issue",
|
||||
"decoderValidationBasis": "official_interface_runtime_sources_and_synthetic_fixtures",
|
||||
"historicalRuntimeReference": "agave_v2_0_0_gated_verifier",
|
||||
"currentRuntimeReference": "agave_v4_1_1_no_op_success_stub",
|
||||
"currentRuntimeBehavior": "no_op_success_stub",
|
||||
"cryptographicVerificationPerformedByDecoder": false,
|
||||
"cryptographicVerificationClaim": verification_claim(input),
|
||||
"runtimeExecutionOutcome": runtime_execution_outcome(input),
|
||||
}),
|
||||
SOURCE,
|
||||
);
|
||||
}
|
||||
|
||||
fn decode_close_context_state(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
bytes: &[u8],
|
||||
) -> crate::DecoderExecutionResult {
|
||||
let roles = [
|
||||
crate::SolanaCoreAccountRole::new("proof_context_state", true, false),
|
||||
crate::SolanaCoreAccountRole::new("lamport_destination", true, false),
|
||||
crate::SolanaCoreAccountRole::new("context_state_authority", false, true),
|
||||
];
|
||||
let accounts = match crate::solana_core_resolve_accounts(
|
||||
input,
|
||||
roles.as_slice(),
|
||||
0,
|
||||
std::option::Option::None,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some("close_context_state"),
|
||||
"zk_token_proof_close_accounts_invalid",
|
||||
error.to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
let account_array = match accounts.as_array() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some("close_context_state"),
|
||||
"zk_token_proof_close_accounts_not_array",
|
||||
"resolved ZK Token Proof close accounts are not an array",
|
||||
);
|
||||
},
|
||||
};
|
||||
let context_key = account_array
|
||||
.first()
|
||||
.and_then(|value| return value.get("accountKey"))
|
||||
.and_then(serde_json::Value::as_str);
|
||||
let destination_key = account_array
|
||||
.get(1)
|
||||
.and_then(|value| return value.get("accountKey"))
|
||||
.and_then(serde_json::Value::as_str);
|
||||
let historical_account_layout_valid = account_array.len() >= 3
|
||||
&& context_key.is_some()
|
||||
&& destination_key.is_some()
|
||||
&& context_key != destination_key;
|
||||
let trailing = match bytes.get(1..) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => &[],
|
||||
};
|
||||
return crate::solana_core_decoded_result(
|
||||
input,
|
||||
crate::SOLANA_CORE_ZK_TOKEN_PROOF_SURFACE_CODE,
|
||||
"close_context_state",
|
||||
crate::EventFamily::Audit,
|
||||
true,
|
||||
accounts,
|
||||
serde_json::json!({
|
||||
"instructionMode": "historical_close_context_state",
|
||||
"historicalAccountLayoutValid": historical_account_layout_valid,
|
||||
"historicalRuntimeAcceptsTrailingBytes": true,
|
||||
"trailingByteLength": trailing.len(),
|
||||
"trailingSha256": crate::solana_core_hash_bytes(trailing),
|
||||
"trailingPrefixHex": crate::solana_core_bounded_hexadecimal_prefix(
|
||||
trailing,
|
||||
PROOF_COMPONENT_PREFIX_BYTES,
|
||||
),
|
||||
"historicalIntendedMutation": "close_context_and_reclaim_lamports",
|
||||
"historicalInnerInstructionSupport": true,
|
||||
"historicalActivationEvidence": "no_public_activation_epoch_documented_in_feature_gate_issue",
|
||||
"decoderValidationBasis": "official_interface_runtime_sources_and_synthetic_fixtures",
|
||||
"historicalRuntimeReference": "agave_v2_0_0_gated_verifier",
|
||||
"currentRuntimeReference": "agave_v4_1_1_no_op_success_stub",
|
||||
"currentRuntimeBehavior": "no_op_success_stub",
|
||||
"currentRuntimeMutation": "none",
|
||||
"stateMutationClaim": "not_asserted",
|
||||
"runtimeExecutionOutcome": runtime_execution_outcome(input),
|
||||
}),
|
||||
SOURCE,
|
||||
);
|
||||
}
|
||||
|
||||
fn decode_verify_proof(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
bytes: &[u8],
|
||||
descriptor: ProofDescriptor,
|
||||
) -> crate::DecoderExecutionResult {
|
||||
let external_proof = bytes.len() == PROOF_ACCOUNT_INSTRUCTION_BYTES;
|
||||
let account_count = match input.instruction_accounts_json.as_array() {
|
||||
std::option::Option::Some(value) => value.len(),
|
||||
std::option::Option::None => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some(descriptor.entry_code),
|
||||
"zk_token_proof_accounts_not_array",
|
||||
"ZK Token Proof instruction accounts are not an array",
|
||||
);
|
||||
},
|
||||
};
|
||||
let historical_account_layout = historical_account_layout(external_proof, account_count);
|
||||
let context_state_requested =
|
||||
if external_proof { account_count >= 3 } else { account_count >= 2 };
|
||||
let mut roles = std::vec::Vec::new();
|
||||
if external_proof {
|
||||
roles.push(crate::SolanaCoreAccountRole::new("proof_data_account", false, false));
|
||||
}
|
||||
if context_state_requested {
|
||||
roles.push(crate::SolanaCoreAccountRole::new("proof_context_state", true, false));
|
||||
roles.push(crate::SolanaCoreAccountRole::new("context_state_authority", false, false));
|
||||
}
|
||||
let accounts = match crate::solana_core_resolve_accounts(
|
||||
input,
|
||||
roles.as_slice(),
|
||||
0,
|
||||
std::option::Option::None,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some(descriptor.entry_code),
|
||||
"zk_token_proof_accounts_invalid",
|
||||
error.to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
let proof_body_size = match descriptor.proof_data_size.checked_sub(descriptor.context_data_size)
|
||||
{
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some(descriptor.entry_code),
|
||||
"zk_token_proof_official_size_inconsistent",
|
||||
"historical ZK Token Proof context size exceeds proof data size",
|
||||
);
|
||||
},
|
||||
};
|
||||
let proof_parameters = if external_proof {
|
||||
let offset = match crate::solana_core_read_u32_le(bytes, 1) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some(descriptor.entry_code),
|
||||
"zk_token_proof_account_offset_invalid",
|
||||
error.to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
serde_json::json!({
|
||||
"proofDataSource": "account",
|
||||
"proofAccountOffset": offset,
|
||||
"proofDataAvailability": "external_account_not_captured_by_transaction_core",
|
||||
"proofDataByteLength": descriptor.proof_data_size,
|
||||
"contextDataByteLength": descriptor.context_data_size,
|
||||
"proofBodyByteLength": proof_body_size,
|
||||
"proofDataSha256": serde_json::Value::Null,
|
||||
"contextDataSha256": serde_json::Value::Null,
|
||||
"proofBodySha256": serde_json::Value::Null,
|
||||
})
|
||||
} else {
|
||||
let proof_data = match bytes.get(1..) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => &[],
|
||||
};
|
||||
let context_data = match proof_data.get(..descriptor.context_data_size) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some(descriptor.entry_code),
|
||||
"zk_token_proof_inline_context_truncated",
|
||||
"inline historical ZK Token Proof context data is truncated",
|
||||
);
|
||||
},
|
||||
};
|
||||
let proof_body = match proof_data.get(descriptor.context_data_size..) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return crate::solana_core_failed_result(
|
||||
std::option::Option::Some(descriptor.entry_code),
|
||||
"zk_token_proof_inline_body_truncated",
|
||||
"inline historical ZK Token Proof body is truncated",
|
||||
);
|
||||
},
|
||||
};
|
||||
serde_json::json!({
|
||||
"proofDataSource": "instruction_data",
|
||||
"proofAccountOffset": serde_json::Value::Null,
|
||||
"proofDataAvailability": "inline_hashed_not_retained",
|
||||
"proofDataByteLength": proof_data.len(),
|
||||
"contextDataByteLength": context_data.len(),
|
||||
"proofBodyByteLength": proof_body.len(),
|
||||
"proofDataSha256": crate::solana_core_hash_bytes(proof_data),
|
||||
"proofDataPrefixHex": crate::solana_core_bounded_hexadecimal_prefix(
|
||||
proof_data,
|
||||
PROOF_COMPONENT_PREFIX_BYTES,
|
||||
),
|
||||
"contextDataSha256": crate::solana_core_hash_bytes(context_data),
|
||||
"contextDataPrefixHex": crate::solana_core_bounded_hexadecimal_prefix(
|
||||
context_data,
|
||||
PROOF_COMPONENT_PREFIX_BYTES,
|
||||
),
|
||||
"proofBodySha256": crate::solana_core_hash_bytes(proof_body),
|
||||
"proofBodyPrefixHex": crate::solana_core_bounded_hexadecimal_prefix(
|
||||
proof_body,
|
||||
PROOF_COMPONENT_PREFIX_BYTES,
|
||||
),
|
||||
})
|
||||
};
|
||||
let historical_variant_availability = if external_proof {
|
||||
"gated_by_enable_zk_proof_from_account"
|
||||
} else {
|
||||
descriptor.historical_inline_availability
|
||||
};
|
||||
return crate::solana_core_decoded_result(
|
||||
input,
|
||||
crate::SOLANA_CORE_ZK_TOKEN_PROOF_SURFACE_CODE,
|
||||
descriptor.entry_code,
|
||||
crate::EventFamily::Audit,
|
||||
true,
|
||||
accounts,
|
||||
serde_json::json!({
|
||||
"proofType": descriptor.entry_code,
|
||||
"proof": proof_parameters,
|
||||
"historicalAccountLayout": historical_account_layout,
|
||||
"historicalAccountLayoutValid": historical_account_layout != "invalid_for_historical_interface",
|
||||
"contextStateRequested": context_state_requested,
|
||||
"historicalContextStateMutation": if context_state_requested {
|
||||
"initialize_after_successful_verification"
|
||||
} else {
|
||||
"none"
|
||||
},
|
||||
"historicalVariantAvailability": historical_variant_availability,
|
||||
"historicalInnerInstructionSupport": false,
|
||||
"historicalActivationEvidence": "no_public_activation_epoch_documented_in_feature_gate_issue",
|
||||
"decoderValidationBasis": "official_interface_runtime_sources_and_synthetic_fixtures",
|
||||
"historicalRuntimeReference": "agave_v2_0_0_gated_verifier",
|
||||
"currentRuntimeReference": "agave_v4_1_1_no_op_success_stub",
|
||||
"currentRuntimeBehavior": "no_op_success_stub",
|
||||
"cryptographicVerificationPerformedByDecoder": false,
|
||||
"cryptographicVerificationClaim": verification_claim(input),
|
||||
"runtimeExecutionOutcome": runtime_execution_outcome(input),
|
||||
}),
|
||||
SOURCE,
|
||||
);
|
||||
}
|
||||
|
||||
fn historical_account_layout(external_proof: bool, account_count: usize) -> &'static str {
|
||||
if external_proof {
|
||||
return match account_count {
|
||||
1 => "proof_account_without_context",
|
||||
3.. => "proof_account_with_context",
|
||||
_ => "invalid_for_historical_interface",
|
||||
};
|
||||
}
|
||||
return match account_count {
|
||||
0 => "inline_without_context",
|
||||
2.. => "inline_with_context",
|
||||
_ => "invalid_for_historical_interface",
|
||||
};
|
||||
}
|
||||
|
||||
fn runtime_execution_outcome(input: &crate::CoreInstructionReplayInput) -> &'static str {
|
||||
if input.transaction_failed {
|
||||
return "not_asserted_transaction_failed";
|
||||
}
|
||||
return "accepted_in_successful_transaction";
|
||||
}
|
||||
|
||||
fn verification_claim(input: &crate::CoreInstructionReplayInput) -> &'static str {
|
||||
if input.transaction_failed {
|
||||
return "not_asserted_transaction_failed";
|
||||
}
|
||||
return "not_asserted_decoder_does_not_bind_transaction_to_runtime_version";
|
||||
}
|
||||
|
||||
fn proof_descriptor(
|
||||
instruction: HistoricalProofInstruction,
|
||||
) -> std::option::Option<ProofDescriptor> {
|
||||
let descriptor = match instruction {
|
||||
HistoricalProofInstruction::CloseContextState => {
|
||||
return std::option::Option::None;
|
||||
},
|
||||
HistoricalProofInstruction::VerifyZeroBalance => ProofDescriptor {
|
||||
entry_code: "verify_zero_balance",
|
||||
proof_data_size: 192,
|
||||
context_data_size: 96,
|
||||
historical_inline_availability: "historical_runtime_implementation_present_activation_unconfirmed",
|
||||
},
|
||||
HistoricalProofInstruction::VerifyWithdraw => ProofDescriptor {
|
||||
entry_code: "verify_withdraw",
|
||||
proof_data_size: 992,
|
||||
context_data_size: 96,
|
||||
historical_inline_availability: "historical_runtime_implementation_present_activation_unconfirmed",
|
||||
},
|
||||
HistoricalProofInstruction::VerifyCiphertextCiphertextEquality => ProofDescriptor {
|
||||
entry_code: "verify_ciphertext_ciphertext_equality",
|
||||
proof_data_size: 416,
|
||||
context_data_size: 192,
|
||||
historical_inline_availability: "historical_runtime_implementation_present_activation_unconfirmed",
|
||||
},
|
||||
HistoricalProofInstruction::VerifyTransfer => ProofDescriptor {
|
||||
entry_code: "verify_transfer",
|
||||
proof_data_size: 1536,
|
||||
context_data_size: 416,
|
||||
historical_inline_availability: "historical_runtime_implementation_present_activation_unconfirmed",
|
||||
},
|
||||
HistoricalProofInstruction::VerifyTransferWithFee => ProofDescriptor {
|
||||
entry_code: "verify_transfer_with_fee",
|
||||
proof_data_size: 2282,
|
||||
context_data_size: 650,
|
||||
historical_inline_availability: "gated_by_enable_zk_transfer_with_fee",
|
||||
},
|
||||
HistoricalProofInstruction::VerifyPubkeyValidity => ProofDescriptor {
|
||||
entry_code: "verify_pubkey_validity",
|
||||
proof_data_size: 96,
|
||||
context_data_size: 32,
|
||||
historical_inline_availability: "historical_runtime_implementation_present_activation_unconfirmed",
|
||||
},
|
||||
HistoricalProofInstruction::VerifyRangeProofU64 => ProofDescriptor {
|
||||
entry_code: "verify_range_proof_u64",
|
||||
proof_data_size: 704,
|
||||
context_data_size: 32,
|
||||
historical_inline_availability: "historical_runtime_implementation_present_activation_unconfirmed",
|
||||
},
|
||||
HistoricalProofInstruction::VerifyBatchedRangeProofU64 => ProofDescriptor {
|
||||
entry_code: "verify_batched_range_proof_u64",
|
||||
proof_data_size: 936,
|
||||
context_data_size: 264,
|
||||
historical_inline_availability: "historical_runtime_implementation_present_activation_unconfirmed",
|
||||
},
|
||||
HistoricalProofInstruction::VerifyBatchedRangeProofU128 => ProofDescriptor {
|
||||
entry_code: "verify_batched_range_proof_u128",
|
||||
proof_data_size: 1000,
|
||||
context_data_size: 264,
|
||||
historical_inline_availability: "historical_runtime_implementation_present_activation_unconfirmed",
|
||||
},
|
||||
HistoricalProofInstruction::VerifyBatchedRangeProofU256 => ProofDescriptor {
|
||||
entry_code: "verify_batched_range_proof_u256",
|
||||
proof_data_size: 1064,
|
||||
context_data_size: 264,
|
||||
historical_inline_availability: "gated_by_enable_zk_transfer_with_fee",
|
||||
},
|
||||
HistoricalProofInstruction::VerifyCiphertextCommitmentEquality => ProofDescriptor {
|
||||
entry_code: "verify_ciphertext_commitment_equality",
|
||||
proof_data_size: 320,
|
||||
context_data_size: 128,
|
||||
historical_inline_availability: "historical_runtime_implementation_present_activation_unconfirmed",
|
||||
},
|
||||
HistoricalProofInstruction::VerifyGroupedCiphertext2HandlesValidity => ProofDescriptor {
|
||||
entry_code: "verify_grouped_ciphertext_2_handles_validity",
|
||||
proof_data_size: 320,
|
||||
context_data_size: 160,
|
||||
historical_inline_availability: "historical_runtime_implementation_present_activation_unconfirmed",
|
||||
},
|
||||
HistoricalProofInstruction::VerifyBatchedGroupedCiphertext2HandlesValidity => {
|
||||
ProofDescriptor {
|
||||
entry_code: "verify_batched_grouped_ciphertext_2_handles_validity",
|
||||
proof_data_size: 416,
|
||||
context_data_size: 256,
|
||||
historical_inline_availability: "historical_runtime_implementation_present_activation_unconfirmed",
|
||||
}
|
||||
},
|
||||
HistoricalProofInstruction::VerifyFeeSigma => ProofDescriptor {
|
||||
entry_code: "verify_fee_sigma",
|
||||
proof_data_size: 360,
|
||||
context_data_size: 104,
|
||||
historical_inline_availability: "gated_by_enable_zk_transfer_with_fee",
|
||||
},
|
||||
HistoricalProofInstruction::VerifyGroupedCiphertext3HandlesValidity => ProofDescriptor {
|
||||
entry_code: "verify_grouped_ciphertext_3_handles_validity",
|
||||
proof_data_size: 416,
|
||||
context_data_size: 224,
|
||||
historical_inline_availability: "historical_runtime_implementation_present_activation_unconfirmed",
|
||||
},
|
||||
HistoricalProofInstruction::VerifyBatchedGroupedCiphertext3HandlesValidity => {
|
||||
ProofDescriptor {
|
||||
entry_code: "verify_batched_grouped_ciphertext_3_handles_validity",
|
||||
proof_data_size: 544,
|
||||
context_data_size: 352,
|
||||
historical_inline_availability: "historical_runtime_implementation_present_activation_unconfirmed",
|
||||
}
|
||||
},
|
||||
};
|
||||
return std::option::Option::Some(descriptor);
|
||||
}
|
||||
|
||||
fn instruction_entries() -> &'static [(u8, &'static str)] {
|
||||
return &[
|
||||
(0, "close_context_state"),
|
||||
(1, "verify_zero_balance"),
|
||||
(2, "verify_withdraw"),
|
||||
(3, "verify_ciphertext_ciphertext_equality"),
|
||||
(4, "verify_transfer"),
|
||||
(5, "verify_transfer_with_fee"),
|
||||
(6, "verify_pubkey_validity"),
|
||||
(7, "verify_range_proof_u64"),
|
||||
(8, "verify_batched_range_proof_u64"),
|
||||
(9, "verify_batched_range_proof_u128"),
|
||||
(10, "verify_batched_range_proof_u256"),
|
||||
(11, "verify_ciphertext_commitment_equality"),
|
||||
(12, "verify_grouped_ciphertext_2_handles_validity"),
|
||||
(13, "verify_batched_grouped_ciphertext_2_handles_validity"),
|
||||
(14, "verify_fee_sigma"),
|
||||
(15, "verify_grouped_ciphertext_3_handles_validity"),
|
||||
(16, "verify_batched_grouped_ciphertext_3_handles_validity"),
|
||||
];
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use base64::Engine; // rust-rules: trait-import
|
||||
|
||||
fn replay_input(
|
||||
bytes: &[u8],
|
||||
account_count: usize,
|
||||
transaction_failed: bool,
|
||||
instruction_path: &str,
|
||||
) -> crate::CoreInstructionReplayInput {
|
||||
let account_keys = (0..account_count)
|
||||
.map(|index| {
|
||||
return serde_json::json!({
|
||||
"accountIndex": index,
|
||||
"accountKey": format!("ZkToken{index:02}1111111111111111111111111"),
|
||||
"source": "static",
|
||||
"writable": index < 2,
|
||||
"signer": index == 2,
|
||||
"executable": false,
|
||||
});
|
||||
})
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
let instruction_accounts = account_keys
|
||||
.iter()
|
||||
.map(|account| {
|
||||
return serde_json::json!({
|
||||
"accountIndex": account["accountIndex"],
|
||||
"accountKey": account["accountKey"],
|
||||
});
|
||||
})
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
let result = crate::CoreInstructionReplayInput::new(
|
||||
format!("signature:{instruction_path}"),
|
||||
"signature",
|
||||
42,
|
||||
instruction_path,
|
||||
kb_program_ids::ZK_TOKEN_PROOF_PROGRAM_ID,
|
||||
transaction_failed,
|
||||
if transaction_failed {
|
||||
std::option::Option::Some(serde_json::json!({"InstructionError": [0, "Custom"]}))
|
||||
} else {
|
||||
std::option::Option::None
|
||||
},
|
||||
serde_json::Value::Array(account_keys),
|
||||
serde_json::Value::Array(instruction_accounts),
|
||||
std::option::Option::Some(serde_json::json!({
|
||||
"dataBase64": base64::engine::general_purpose::STANDARD.encode(bytes),
|
||||
})),
|
||||
std::option::Option::Some("payload-hash".to_string()),
|
||||
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!("ZK Token Proof replay input failed: {error}")
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn inline_fixture(instruction: super::HistoricalProofInstruction) -> std::vec::Vec<u8> {
|
||||
let descriptor = match super::proof_descriptor(instruction) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => panic!("proof descriptor missing"),
|
||||
};
|
||||
let inline_size = match descriptor.proof_data_size.checked_add(1) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => panic!("inline fixture size overflow"),
|
||||
};
|
||||
let tag = instruction as u8;
|
||||
let mut bytes = vec![tag];
|
||||
bytes.resize(inline_size, tag);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coverage_declares_seventeen_historical_entries_and_current_noop_fallback() {
|
||||
let coverage = crate::solana_core_zk_token_proof_coverage();
|
||||
assert_eq!(coverage.len(), 18);
|
||||
assert_eq!(coverage.iter().filter(|entry| return entry.historical).count(), 17);
|
||||
assert!(coverage.iter().any(|entry| {
|
||||
return entry.entry_code == "current_runtime_noop_invocation"
|
||||
&& !entry.historical
|
||||
&& entry.discriminator_hex.is_none();
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn official_discriminants_map_to_stable_entry_codes() {
|
||||
for (tag, entry_code) in super::instruction_entries() {
|
||||
let bytes = if *tag == 0 { vec![*tag] } else { vec![*tag, 0, 0, 0, 0] };
|
||||
let recognition = crate::solana_core_zk_token_proof_recognize(
|
||||
&replay_input(bytes.as_slice(), 3, false, "0"),
|
||||
100,
|
||||
);
|
||||
assert!(recognition.compatible);
|
||||
assert_eq!(recognition.entry_code.as_deref(), std::option::Option::Some(*entry_code));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_historical_inline_layout_matches_the_audited_wire_table() {
|
||||
let variants = [
|
||||
(super::HistoricalProofInstruction::VerifyZeroBalance, 192, 96),
|
||||
(super::HistoricalProofInstruction::VerifyWithdraw, 992, 96),
|
||||
(super::HistoricalProofInstruction::VerifyCiphertextCiphertextEquality, 416, 192),
|
||||
(super::HistoricalProofInstruction::VerifyTransfer, 1536, 416),
|
||||
(super::HistoricalProofInstruction::VerifyTransferWithFee, 2282, 650),
|
||||
(super::HistoricalProofInstruction::VerifyPubkeyValidity, 96, 32),
|
||||
(super::HistoricalProofInstruction::VerifyRangeProofU64, 704, 32),
|
||||
(super::HistoricalProofInstruction::VerifyBatchedRangeProofU64, 936, 264),
|
||||
(super::HistoricalProofInstruction::VerifyBatchedRangeProofU128, 1000, 264),
|
||||
(super::HistoricalProofInstruction::VerifyBatchedRangeProofU256, 1064, 264),
|
||||
(super::HistoricalProofInstruction::VerifyCiphertextCommitmentEquality, 320, 128),
|
||||
(
|
||||
super::HistoricalProofInstruction::VerifyGroupedCiphertext2HandlesValidity,
|
||||
320,
|
||||
160,
|
||||
),
|
||||
(
|
||||
super::HistoricalProofInstruction::VerifyBatchedGroupedCiphertext2HandlesValidity,
|
||||
416,
|
||||
256,
|
||||
),
|
||||
(super::HistoricalProofInstruction::VerifyFeeSigma, 360, 104),
|
||||
(
|
||||
super::HistoricalProofInstruction::VerifyGroupedCiphertext3HandlesValidity,
|
||||
416,
|
||||
224,
|
||||
),
|
||||
(
|
||||
super::HistoricalProofInstruction::VerifyBatchedGroupedCiphertext3HandlesValidity,
|
||||
544,
|
||||
352,
|
||||
),
|
||||
];
|
||||
for (instruction, proof_data_size, context_data_size) in variants {
|
||||
let descriptor = match super::proof_descriptor(instruction) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => panic!("proof descriptor missing"),
|
||||
};
|
||||
assert_eq!(descriptor.proof_data_size, proof_data_size);
|
||||
assert_eq!(descriptor.context_data_size, context_data_size);
|
||||
let bytes = inline_fixture(instruction);
|
||||
let result = crate::solana_core_zk_token_proof_decode(&replay_input(
|
||||
bytes.as_slice(),
|
||||
0,
|
||||
true,
|
||||
"0",
|
||||
));
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
assert_eq!(result.observations.len(), 1);
|
||||
assert!(!result.observations[0].observation_committed);
|
||||
assert_eq!(
|
||||
result.observations[0].payload_json["parameters"]["proof"]["proofDataByteLength"],
|
||||
bytes.len() - 1
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proof_account_mode_is_structured_and_historical_feature_gate_is_explicit() {
|
||||
let result = crate::solana_core_zk_token_proof_decode(&replay_input(
|
||||
&[1, 44, 0, 0, 0],
|
||||
3,
|
||||
false,
|
||||
"0",
|
||||
));
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
let parameters = &result.observations[0].payload_json["parameters"];
|
||||
assert_eq!(parameters["proof"]["proofDataSource"], "account");
|
||||
assert_eq!(parameters["proof"]["proofAccountOffset"], 44);
|
||||
assert_eq!(
|
||||
parameters["historicalVariantAvailability"],
|
||||
"gated_by_enable_zk_proof_from_account"
|
||||
);
|
||||
assert!(parameters["contextStateRequested"].as_bool().is_some_and(|value| return value));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn historical_transfer_fee_gates_are_explicit() {
|
||||
let variants = [
|
||||
super::HistoricalProofInstruction::VerifyTransferWithFee,
|
||||
super::HistoricalProofInstruction::VerifyBatchedRangeProofU256,
|
||||
super::HistoricalProofInstruction::VerifyFeeSigma,
|
||||
];
|
||||
for instruction in variants {
|
||||
let bytes = inline_fixture(instruction);
|
||||
let result = crate::solana_core_zk_token_proof_decode(&replay_input(
|
||||
bytes.as_slice(),
|
||||
0,
|
||||
false,
|
||||
"0",
|
||||
));
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
assert_eq!(
|
||||
result.observations[0].payload_json["parameters"]["historicalVariantAvailability"],
|
||||
"gated_by_enable_zk_transfer_with_fee"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_historical_shapes_decode_as_current_noop_instead_of_false_failure() {
|
||||
let fixtures: &[&[u8]] = &[&[], &[99], &[1, 2, 3]];
|
||||
for bytes in fixtures {
|
||||
let result =
|
||||
crate::solana_core_zk_token_proof_decode(&replay_input(bytes, 1, false, "0"));
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
assert_eq!(
|
||||
result.recognized_entry_code.as_deref(),
|
||||
std::option::Option::Some("current_runtime_noop_invocation")
|
||||
);
|
||||
assert_eq!(
|
||||
result.observations[0].payload_json["parameters"]["currentRuntimeReference"],
|
||||
"agave_v4_1_1_no_op_success_stub"
|
||||
);
|
||||
assert_eq!(
|
||||
result.observations[0].payload_json["parameters"]["currentRuntimeBehavior"],
|
||||
"no_op_success_stub"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn historical_account_arity_is_reported_without_overriding_current_noop_acceptance() {
|
||||
let inline = inline_fixture(super::HistoricalProofInstruction::VerifyZeroBalance);
|
||||
let inline_result = crate::solana_core_zk_token_proof_decode(&replay_input(
|
||||
inline.as_slice(),
|
||||
1,
|
||||
false,
|
||||
"0",
|
||||
));
|
||||
assert_eq!(inline_result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
assert!(inline_result.observations[0].payload_json["parameters"]["historicalAccountLayoutValid"]
|
||||
.as_bool()
|
||||
.is_some_and(|value| return !value));
|
||||
let account_result = crate::solana_core_zk_token_proof_decode(&replay_input(
|
||||
&[1, 0, 0, 0, 0],
|
||||
2,
|
||||
false,
|
||||
"0",
|
||||
));
|
||||
assert_eq!(account_result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
assert!(account_result.observations[0].payload_json["parameters"]["historicalAccountLayoutValid"]
|
||||
.as_bool()
|
||||
.is_some_and(|value| return !value));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn close_context_state_preserves_historical_intent_without_claiming_mutation() {
|
||||
let result =
|
||||
crate::solana_core_zk_token_proof_decode(&replay_input(&[0, 7, 8], 3, false, "0"));
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
let parameters = &result.observations[0].payload_json["parameters"];
|
||||
assert!(
|
||||
parameters["historicalAccountLayoutValid"]
|
||||
.as_bool()
|
||||
.is_some_and(|value| return value)
|
||||
);
|
||||
assert_eq!(parameters["trailingByteLength"], 2);
|
||||
assert!(
|
||||
parameters["historicalInnerInstructionSupport"]
|
||||
.as_bool()
|
||||
.is_some_and(|value| return value)
|
||||
);
|
||||
assert_eq!(parameters["currentRuntimeMutation"], "none");
|
||||
assert_eq!(parameters["stateMutationClaim"], "not_asserted");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inner_invocation_never_claims_historical_verification() {
|
||||
let bytes = inline_fixture(super::HistoricalProofInstruction::VerifyPubkeyValidity);
|
||||
let result = crate::solana_core_zk_token_proof_decode(&replay_input(
|
||||
bytes.as_slice(),
|
||||
0,
|
||||
false,
|
||||
"0/1",
|
||||
));
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
assert!(
|
||||
result.observations[0].payload_json["parameters"]["historicalInnerInstructionSupport"]
|
||||
.as_bool()
|
||||
.is_some_and(|value| return !value)
|
||||
);
|
||||
assert_eq!(
|
||||
result.observations[0].payload_json["parameters"]["cryptographicVerificationClaim"],
|
||||
"not_asserted_decoder_does_not_bind_transaction_to_runtime_version"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_transaction_is_non_committed_and_never_claims_verification() {
|
||||
let bytes = inline_fixture(super::HistoricalProofInstruction::VerifyZeroBalance);
|
||||
let result =
|
||||
crate::solana_core_zk_token_proof_decode(&replay_input(bytes.as_slice(), 0, true, "0"));
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
assert!(!result.observations[0].observation_committed);
|
||||
assert_eq!(
|
||||
result.observations[0].payload_json["parameters"]["cryptographicVerificationClaim"],
|
||||
"not_asserted_transaction_failed"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialization_is_deterministic() {
|
||||
let input = replay_input(&[1, 0, 0, 0, 0], 1, false, "0");
|
||||
let first = crate::solana_core_zk_token_proof_decode(&input);
|
||||
let second = crate::solana_core_zk_token_proof_decode(&input);
|
||||
let first_json = match serde_json::to_string(&first.observations[0].payload_json) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("first serialization failed: {error}"),
|
||||
};
|
||||
let second_json = match serde_json::to_string(&second.observations[0].payload_json) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("second serialization failed: {error}"),
|
||||
};
|
||||
assert_eq!(first_json, second_json);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-lib/src/lib.rs
|
||||
// version: 3
|
||||
// version: 6
|
||||
|
||||
//! Consolidated decoder, executor, materializer and shared model library.
|
||||
#![warn(missing_docs)]
|
||||
@@ -11,10 +11,179 @@ pub mod executor;
|
||||
pub mod materializer;
|
||||
pub mod model;
|
||||
|
||||
/// Canonical tracing target for the consolidated library.
|
||||
pub(crate) use crate::decoder::api::constants::TRACING_TARGET;
|
||||
/// Stable Address Lookup Table surface code.
|
||||
pub(crate) use crate::decoder::solana::SOLANA_CORE_ADDRESS_LOOKUP_TABLE_SURFACE_CODE;
|
||||
/// Stable deprecated immutable BPF Loader surface code.
|
||||
pub(crate) use crate::decoder::solana::SOLANA_CORE_BPF_LOADER_DEPRECATED_SURFACE_CODE;
|
||||
/// Stable immutable BPF Loader v2 surface code.
|
||||
pub(crate) use crate::decoder::solana::SOLANA_CORE_BPF_LOADER_SURFACE_CODE;
|
||||
/// Stable upgradeable BPF Loader surface code.
|
||||
pub(crate) use crate::decoder::solana::SOLANA_CORE_BPF_LOADER_UPGRADEABLE_SURFACE_CODE;
|
||||
/// Stable Compute Budget surface code.
|
||||
pub(crate) use crate::decoder::solana::SOLANA_CORE_COMPUTE_BUDGET_SURFACE_CODE;
|
||||
/// Stable Config Program surface code.
|
||||
pub(crate) use crate::decoder::solana::SOLANA_CORE_CONFIG_SURFACE_CODE;
|
||||
/// Byte length of an Ed25519 public key.
|
||||
pub(crate) use crate::decoder::solana::SOLANA_CORE_ED25519_PUBLIC_KEY_BYTES;
|
||||
/// Stable Ed25519 signature precompile surface code.
|
||||
pub(crate) use crate::decoder::solana::SOLANA_CORE_ED25519_SURFACE_CODE;
|
||||
/// Stable Feature Gate surface code.
|
||||
pub(crate) use crate::decoder::solana::SOLANA_CORE_FEATURE_SURFACE_CODE;
|
||||
/// Stable Loader v4 surface code.
|
||||
pub(crate) use crate::decoder::solana::SOLANA_CORE_LOADER_V4_SURFACE_CODE;
|
||||
/// Maximum retained native instruction payload accepted by the first decoder phase.
|
||||
pub(crate) use crate::decoder::solana::SOLANA_CORE_MAX_NATIVE_INSTRUCTION_PAYLOAD_BYTES;
|
||||
/// Current native event payload contract version.
|
||||
pub(crate) use crate::decoder::solana::SOLANA_CORE_NATIVE_EVENT_VERSION;
|
||||
/// Stable Native Loader surface code.
|
||||
pub(crate) use crate::decoder::solana::SOLANA_CORE_NATIVE_LOADER_SURFACE_CODE;
|
||||
/// Maximum number of component bytes retained as a hexadecimal event prefix.
|
||||
pub(crate) use crate::decoder::solana::SOLANA_CORE_PRECOMPILE_COMPONENT_PREFIX_BYTES;
|
||||
/// Expected role and privileges for one positional instruction account.
|
||||
pub(crate) use crate::decoder::solana::SolanaCoreAccountRole;
|
||||
/// Returns declared Address Lookup Table instruction coverage.
|
||||
pub(crate) use crate::decoder::solana::solana_core_address_lookup_table_coverage;
|
||||
/// Decodes one Address Lookup Table instruction.
|
||||
pub(crate) use crate::decoder::solana::solana_core_address_lookup_table_decode;
|
||||
/// Recognizes one Address Lookup Table instruction without producing an event.
|
||||
pub(crate) use crate::decoder::solana::solana_core_address_lookup_table_recognize;
|
||||
/// Returns declared Compute Budget instruction coverage.
|
||||
pub(crate) use crate::decoder::solana::solana_core_compute_budget_coverage;
|
||||
/// Decodes one Compute Budget instruction.
|
||||
pub(crate) use crate::decoder::solana::solana_core_compute_budget_decode;
|
||||
/// Recognizes one Compute Budget instruction without producing an event.
|
||||
pub(crate) use crate::decoder::solana::solana_core_compute_budget_recognize;
|
||||
/// Returns declared Config Program instruction coverage.
|
||||
pub(crate) use crate::decoder::solana::solana_core_config_coverage;
|
||||
/// Decodes one generic Config Program store instruction.
|
||||
pub(crate) use crate::decoder::solana::solana_core_config_decode;
|
||||
/// Recognizes one generic Config Program store instruction.
|
||||
pub(crate) use crate::decoder::solana::solana_core_config_recognize;
|
||||
/// Resolves positional instruction accounts and validates their core indexes.
|
||||
pub(crate) use crate::decoder::solana::solana_core_resolve_accounts;
|
||||
|
||||
/// Stable protocol code shared by native Solana events.
|
||||
pub(crate) use crate::decoder::solana::SOLANA_CORE_PROTOCOL_CODE;
|
||||
/// Byte length of a secp256k1 Ethereum address.
|
||||
pub(crate) use crate::decoder::solana::SOLANA_CORE_SECP256K1_ETHEREUM_ADDRESS_BYTES;
|
||||
/// Stable secp256k1 signature precompile surface code.
|
||||
pub(crate) use crate::decoder::solana::SOLANA_CORE_SECP256K1_SURFACE_CODE;
|
||||
/// Runtime maximum number of secp256r1 signatures in one precompile instruction.
|
||||
pub(crate) use crate::decoder::solana::SOLANA_CORE_SECP256R1_MAX_SIGNATURES;
|
||||
/// Byte length of a compressed secp256r1 public key.
|
||||
pub(crate) use crate::decoder::solana::SOLANA_CORE_SECP256R1_PUBLIC_KEY_BYTES;
|
||||
/// Stable secp256r1 signature precompile surface code.
|
||||
pub(crate) use crate::decoder::solana::SOLANA_CORE_SECP256R1_SURFACE_CODE;
|
||||
/// Byte length shared by compact Ed25519, secp256k1 and secp256r1 signatures.
|
||||
pub(crate) use crate::decoder::solana::SOLANA_CORE_SIGNATURE_BYTES;
|
||||
/// Stable Slashing Program decoder surface code.
|
||||
pub(crate) use crate::decoder::solana::SOLANA_CORE_SLASHING_SURFACE_CODE;
|
||||
/// Stable Stake Program surface code.
|
||||
pub(crate) use crate::decoder::solana::SOLANA_CORE_STAKE_SURFACE_CODE;
|
||||
/// Stable System Program surface code.
|
||||
pub(crate) use crate::decoder::solana::SOLANA_CORE_SYSTEM_SURFACE_CODE;
|
||||
/// Canonical tracing target for this crate.
|
||||
pub(crate) use crate::decoder::solana::SOLANA_CORE_TRACING_TARGET;
|
||||
/// Byte length of one secp256k1 offsets entry.
|
||||
pub(crate) use crate::decoder::solana::SOLANA_CORE_U8_OFFSETS_BYTES;
|
||||
/// Byte length of one Ed25519 or secp256r1 offsets entry.
|
||||
pub(crate) use crate::decoder::solana::SOLANA_CORE_U16_OFFSETS_BYTES;
|
||||
/// Stable Vote Program surface code.
|
||||
pub(crate) use crate::decoder::solana::SOLANA_CORE_VOTE_SURFACE_CODE;
|
||||
/// Stable native ZK ElGamal Proof surface code.
|
||||
pub(crate) use crate::decoder::solana::SOLANA_CORE_ZK_ELGAMAL_PROOF_SURFACE_CODE;
|
||||
/// Stable historical ZK Token Proof surface code.
|
||||
pub(crate) use crate::decoder::solana::SOLANA_CORE_ZK_TOKEN_PROOF_SURFACE_CODE;
|
||||
/// One resolved outer instruction payload and its provenance relative to the target instruction.
|
||||
pub(crate) use crate::decoder::solana::SolanaCoreResolvedInstructionPayload;
|
||||
/// Returns a hexadecimal prefix no longer than the available byte slice.
|
||||
pub(crate) use crate::decoder::solana::solana_core_bounded_hexadecimal_prefix;
|
||||
/// Extracts one exact bounded byte slice with checked arithmetic.
|
||||
pub(crate) use crate::decoder::solana::solana_core_bounded_slice;
|
||||
/// Decodes one retained base64 instruction payload with an explicit byte limit.
|
||||
pub(crate) use crate::decoder::solana::solana_core_decode_instruction_data;
|
||||
/// Returns the decoded target payload length when retained and valid.
|
||||
pub(crate) use crate::decoder::solana::solana_core_decoded_payload_length;
|
||||
/// Returns the SHA-256 of the decoded target instruction data when available.
|
||||
pub(crate) use crate::decoder::solana::solana_core_decoded_payload_sha256;
|
||||
/// Builds one exact decoded native observation.
|
||||
pub(crate) use crate::decoder::solana::solana_core_decoded_result;
|
||||
/// Builds one failed native decode result.
|
||||
pub(crate) use crate::decoder::solana::solana_core_failed_result;
|
||||
/// Returns declared Feature Gate instruction coverage.
|
||||
pub(crate) use crate::decoder::solana::solana_core_feature_coverage;
|
||||
/// Decodes one Feature Gate instruction.
|
||||
pub(crate) use crate::decoder::solana::solana_core_feature_decode;
|
||||
/// Recognizes one Feature Gate instruction without producing an event.
|
||||
pub(crate) use crate::decoder::solana::solana_core_feature_recognize;
|
||||
/// Returns the lowercase SHA-256 of one bounded byte slice.
|
||||
pub(crate) use crate::decoder::solana::solana_core_hash_bytes;
|
||||
/// Returns a normalized lowercase hexadecimal prefix.
|
||||
pub(crate) use crate::decoder::solana::solana_core_hexadecimal_prefix;
|
||||
/// Returns declared loader instruction coverage.
|
||||
pub(crate) use crate::decoder::solana::solana_core_loaders_coverage;
|
||||
/// Decodes one loader instruction.
|
||||
pub(crate) use crate::decoder::solana::solana_core_loaders_decode;
|
||||
/// Recognizes one loader instruction without producing an event.
|
||||
pub(crate) use crate::decoder::solana::solana_core_loaders_recognize;
|
||||
/// Returns a stable payload hash from core or computes it from retained JSON.
|
||||
pub(crate) use crate::decoder::solana::solana_core_payload_hash;
|
||||
/// Returns declared signature precompile coverage.
|
||||
pub(crate) use crate::decoder::solana::solana_core_precompiles_coverage;
|
||||
/// Decodes one signature precompile instruction structurally without cryptographic recomputation.
|
||||
pub(crate) use crate::decoder::solana::solana_core_precompiles_decode;
|
||||
/// Recognizes one signature precompile instruction without resolving referenced data.
|
||||
pub(crate) use crate::decoder::solana::solana_core_precompiles_recognize;
|
||||
/// Reads one little-endian `u32` from an exact byte range.
|
||||
pub(crate) use crate::decoder::solana::solana_core_read_u32_le;
|
||||
/// Reads one little-endian `u64` from an exact byte range.
|
||||
pub(crate) use crate::decoder::solana::solana_core_read_u64_le;
|
||||
/// Resolves a secp256k1 instruction reference. The runtime format has no current-instruction
|
||||
/// sentinel: every `u8` value is an explicit outer instruction index.
|
||||
pub(crate) use crate::decoder::solana::solana_core_resolve_u8_instruction_payload;
|
||||
/// Resolves an Ed25519 or secp256r1 instruction reference using the official `u16::MAX` sentinel.
|
||||
pub(crate) use crate::decoder::solana::solana_core_resolve_u16_instruction_payload;
|
||||
/// Returns declared Slashing Program instruction coverage.
|
||||
pub(crate) use crate::decoder::solana::solana_core_slashing_coverage;
|
||||
/// Decodes one Slashing Program instruction.
|
||||
pub(crate) use crate::decoder::solana::solana_core_slashing_decode;
|
||||
/// Recognizes one Slashing Program instruction without producing an event.
|
||||
pub(crate) use crate::decoder::solana::solana_core_slashing_recognize;
|
||||
/// Returns declared Stake Program instruction coverage.
|
||||
pub(crate) use crate::decoder::solana::solana_core_stake_coverage;
|
||||
/// Decodes one Stake Program instruction.
|
||||
pub(crate) use crate::decoder::solana::solana_core_stake_decode;
|
||||
/// Recognizes one Stake Program instruction without producing an event.
|
||||
pub(crate) use crate::decoder::solana::solana_core_stake_recognize;
|
||||
/// Returns declared System Program instruction coverage.
|
||||
pub(crate) use crate::decoder::solana::solana_core_system_coverage;
|
||||
/// Decodes one System Program instruction.
|
||||
pub(crate) use crate::decoder::solana::solana_core_system_decode;
|
||||
/// Recognizes one System Program instruction without producing an event.
|
||||
pub(crate) use crate::decoder::solana::solana_core_system_recognize;
|
||||
/// Returns the numeric outer index of the target instruction.
|
||||
pub(crate) use crate::decoder::solana::solana_core_target_outer_instruction_index;
|
||||
/// Builds one unsupported native decode result with a bounded diagnostic.
|
||||
pub(crate) use crate::decoder::solana::solana_core_unsupported_result;
|
||||
/// Returns declared Vote Program instruction coverage.
|
||||
pub(crate) use crate::decoder::solana::solana_core_vote_coverage;
|
||||
/// Decodes one Vote Program instruction.
|
||||
pub(crate) use crate::decoder::solana::solana_core_vote_decode;
|
||||
/// Recognizes one Vote Program instruction without producing an event.
|
||||
pub(crate) use crate::decoder::solana::solana_core_vote_recognize;
|
||||
/// Returns declared ZK ElGamal Proof instruction coverage.
|
||||
pub(crate) use crate::decoder::solana::solana_core_zk_elgamal_coverage;
|
||||
/// Decodes one native ZK ElGamal Proof instruction without recomputing the proof.
|
||||
pub(crate) use crate::decoder::solana::solana_core_zk_elgamal_decode;
|
||||
/// Recognizes one native ZK ElGamal Proof instruction.
|
||||
pub(crate) use crate::decoder::solana::solana_core_zk_elgamal_recognize;
|
||||
/// Returns declared historical and current ZK Token Proof coverage.
|
||||
pub(crate) use crate::decoder::solana::solana_core_zk_token_proof_coverage;
|
||||
/// Decodes one historical ZK Token Proof layout without recomputing any proof.
|
||||
pub(crate) use crate::decoder::solana::solana_core_zk_token_proof_decode;
|
||||
/// Recognizes one historical ZK Token Proof layout or the current no-op runtime fallback.
|
||||
pub(crate) use crate::decoder::solana::solana_core_zk_token_proof_recognize;
|
||||
|
||||
/// Current contextual core instruction input contract version.
|
||||
pub use crate::decoder::api::contracts::CORE_INSTRUCTION_INPUT_CONTRACT_VERSION;
|
||||
/// Stable contextual decoded observation.
|
||||
@@ -55,6 +224,8 @@ pub use crate::decoder::api::decoder::DecoderSupport;
|
||||
pub use crate::decoder::api::decoder::InitialDecoder;
|
||||
/// Exposes the common protocol decoder trait.
|
||||
pub use crate::decoder::api::decoder::ProtocolDecoder;
|
||||
/// Runtime-native Solana decoder with maximal native instruction coverage.
|
||||
pub use crate::decoder::solana::SolanaCoreDecoder;
|
||||
/// Exposes the blockhash policy kind.
|
||||
pub use crate::executor::api::execution::ExecutionBlockhashKind;
|
||||
/// Exposes the blockhash policy.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-lib/src/model/replay.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
//! Source-neutral instruction replay input shared by decoders and stores.
|
||||
|
||||
@@ -123,4 +123,21 @@ impl CoreInstructionReplayInput {
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
/// Adds an optional stable surface hint after validation.
|
||||
pub fn with_surface_code_hint(
|
||||
mut self,
|
||||
surface_code_hint: std::option::Option<std::string::String>,
|
||||
) -> kb_core::Result<Self> {
|
||||
if surface_code_hint
|
||||
.as_deref()
|
||||
.is_some_and(|surface| return surface.trim().is_empty())
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(
|
||||
"core replay input surface hint must not be empty when present",
|
||||
));
|
||||
}
|
||||
self.surface_code_hint = surface_code_hint;
|
||||
return std::result::Result::Ok(self);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user