0.1.0-pre.004
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# file: kb-lib/Cargo.toml
|
||||
# version: 2
|
||||
# version: 3
|
||||
|
||||
[package]
|
||||
name = "kb-lib"
|
||||
@@ -9,14 +9,25 @@ license.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
base64.workspace = true
|
||||
borsh.workspace = true
|
||||
bs58.workspace = true
|
||||
kb-core = { path = "../kb-core" }
|
||||
kb-program-ids = { path = "../kb-program-ids" }
|
||||
base64 = "0.22"
|
||||
bs58 = "0.5"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
sha2 = "0.10"
|
||||
ts-rs = { version = "11", features = ["serde-compat"] }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
sha2.workspace = true
|
||||
solana-address-lookup-table-interface.workspace = true
|
||||
solana-compute-budget-interface.workspace = true
|
||||
solana-loader-v3-interface.workspace = true
|
||||
solana-sdk-ids.workspace = true
|
||||
solana-stake-interface.workspace = true
|
||||
solana-system-interface.workspace = true
|
||||
solana-vote-interface.workspace = true
|
||||
solana-zk-elgamal-proof-interface.workspace = true
|
||||
tracing.workspace = true
|
||||
ts-rs.workspace = true
|
||||
wincode.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
51
kb-lib/README.md
Normal file
51
kb-lib/README.md
Normal file
@@ -0,0 +1,51 @@
|
||||
<!-- file: kb-lib/README.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# kb-lib
|
||||
|
||||
`kb-lib` regroupe les modèles partagés et les composants de décodage, matérialisation et exécution de `khadhroony-bot3`. Les composants restent séparés par modules privés et sont exposés par la façade unique `kb-lib/src/lib.rs`.
|
||||
|
||||
## Décodeur Solana Core
|
||||
|
||||
`SolanaCoreDecoder` est le premier décodeur concret porté depuis bot2. Il implémente `InstructionDecoder` et `ProtocolDecoder` sans dépendre de PostgreSQL, du RPC, de Tauri ou du wallet.
|
||||
|
||||
Il couvre les 18 surfaces natives suivantes :
|
||||
|
||||
- System Program ;
|
||||
- Compute Budget ;
|
||||
- Address Lookup Table ;
|
||||
- Config et Feature Gate ;
|
||||
- Vote et Stake ;
|
||||
- loaders natifs, BPF historiques, upgradeable et Loader v4 ;
|
||||
- précompiles Ed25519, secp256k1 et secp256r1 ;
|
||||
- Slashing ;
|
||||
- ZK ElGamal Proof et l’ancien ZK Token Proof.
|
||||
|
||||
La matrice `docs/NATIVE_SOLANA_DECODER_MATRIX.json` reste la source machine-readable de la couverture. Les instructions inconnues, tronquées, historiques ou issues d’une transaction échouée conservent les statuts et diagnostics explicites définis par les contrats communs.
|
||||
|
||||
## API publique utile
|
||||
|
||||
- `SolanaCoreDecoder` : décodeur concret natif ;
|
||||
- `InstructionDecoder` : contrat de reconnaissance, couverture et décodage contextualisé ;
|
||||
- `ProtocolDecoder` : contrat de compatibilité avec les observations historiques ;
|
||||
- `CoreInstructionReplayInput` : input source-neutral produit par l’extraction core ;
|
||||
- `DecoderExecutionResult`, `DecoderRecognition` et `DecoderCoverageDeclaration` : résultats typés du pipeline ;
|
||||
- modèles canoniques et nomenclature réexportés depuis la façade.
|
||||
|
||||
Les helpers, constantes wire et types intermédiaires de chaque composant restent internes. Leurs noms sont préfixés au niveau de la façade interne afin d’éviter les collisions lors de la fusion des anciens crates.
|
||||
|
||||
## Exemple
|
||||
|
||||
```rust
|
||||
let decoder = kb_lib::SolanaCoreDecoder;
|
||||
let recognition = kb_lib::InstructionDecoder::recognize(&decoder, &input);
|
||||
```
|
||||
|
||||
L’appel de décodage complet utilise `InstructionDecoder::decode` après une reconnaissance compatible. Le pipeline demeure responsable de la sélection du décodeur et de la persistance du résultat.
|
||||
|
||||
## Frontières
|
||||
|
||||
- `kb-lib` ne dépend jamais de `kb-store`.
|
||||
- Les décodeurs ne lisent pas un état RPC courant pour reconstruire une transaction historique.
|
||||
- Une transaction échouée peut produire une intention structurée, jamais une mutation commitée.
|
||||
- Les matérialisateurs et exécuteurs seront portés dans des tranches séparées.
|
||||
@@ -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