0.1.0
This commit is contained in:
2017
migration/khadhroony-bot2-reference/kb_pipeline/src/backfill.rs
Normal file
2017
migration/khadhroony-bot2-reference/kb_pipeline/src/backfill.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
||||
// file: kb_pipeline/src/constants.rs
|
||||
// version: 1
|
||||
|
||||
//! Local constants for the `kb_pipeline` crate.
|
||||
|
||||
/// Canonical tracing target for this crate.
|
||||
pub(crate) const TRACING_TARGET: &str = "kb_pipeline";
|
||||
File diff suppressed because it is too large
Load Diff
2293
migration/khadhroony-bot2-reference/kb_pipeline/src/decode_replay.rs
Normal file
2293
migration/khadhroony-bot2-reference/kb_pipeline/src/decode_replay.rs
Normal file
File diff suppressed because it is too large
Load Diff
323
migration/khadhroony-bot2-reference/kb_pipeline/src/lib.rs
Normal file
323
migration/khadhroony-bot2-reference/kb_pipeline/src/lib.rs
Normal file
@@ -0,0 +1,323 @@
|
||||
// file: kb_pipeline/src/lib.rs
|
||||
// version: 29
|
||||
|
||||
//! Pipeline orchestration primitives.
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
mod backfill;
|
||||
mod constants;
|
||||
mod core_extraction;
|
||||
mod decode_replay;
|
||||
mod plan;
|
||||
mod solana_ata_execution;
|
||||
mod solana_ata_stateful;
|
||||
mod solana_elgamal_registry_stateful;
|
||||
mod solana_execution;
|
||||
mod solana_memo_execution;
|
||||
mod solana_stateful;
|
||||
mod solana_token_2022_correlation;
|
||||
mod solana_token_2022_crypto_preflight;
|
||||
mod solana_token_2022_devnet_execution;
|
||||
mod solana_token_2022_devnet_scenarios;
|
||||
mod solana_token_2022_execution_orchestration;
|
||||
mod solana_token_2022_preflight;
|
||||
mod solana_token_2022_proof_orchestration;
|
||||
mod solana_token_2022_stateful;
|
||||
mod solana_token_2022_validation;
|
||||
mod solana_token_execution;
|
||||
mod solana_token_lifecycle;
|
||||
mod solana_token_stateful;
|
||||
|
||||
/// Canonical tracing target for this crate.
|
||||
pub(crate) use crate::constants::TRACING_TARGET;
|
||||
|
||||
/// Address category used by targeted history backfills.
|
||||
pub use crate::backfill::BackfillAddressKind;
|
||||
/// Chronological direction relative to an anchor signature.
|
||||
pub use crate::backfill::BackfillDirection;
|
||||
/// Application progress and cancellation observer.
|
||||
pub use crate::backfill::BackfillObserver;
|
||||
/// Operator-visible backfill progress event.
|
||||
pub use crate::backfill::BackfillProgressEvent;
|
||||
/// Backfill progress severity.
|
||||
pub use crate::backfill::BackfillProgressLevel;
|
||||
/// Complete bounded backfill request.
|
||||
pub use crate::backfill::BackfillRequest;
|
||||
/// Candidate source used by one campaign.
|
||||
pub use crate::backfill::BackfillSource;
|
||||
/// Final backfill counters and pagination information.
|
||||
pub use crate::backfill::BackfillSummary;
|
||||
/// Executes one bounded HTTP backfill.
|
||||
pub use crate::backfill::execute_http_backfill;
|
||||
/// Stable canonical to core processor name.
|
||||
pub use crate::core_extraction::CORE_EXTRACTION_PROCESSOR_NAME;
|
||||
/// Current canonical to core extractor version.
|
||||
pub use crate::core_extraction::CORE_EXTRACTION_PROCESSOR_VERSION;
|
||||
/// Stable canonical to core processing stage.
|
||||
pub use crate::core_extraction::CORE_EXTRACTION_STAGE;
|
||||
/// Core extraction progress and cancellation observer.
|
||||
pub use crate::core_extraction::CoreExtractionObserver;
|
||||
/// Operator-visible core extraction progress event.
|
||||
pub use crate::core_extraction::CoreExtractionProgressEvent;
|
||||
/// Core extraction progress severity.
|
||||
pub use crate::core_extraction::CoreExtractionProgressLevel;
|
||||
/// Complete bounded core extraction request.
|
||||
pub use crate::core_extraction::CoreExtractionRequest;
|
||||
/// Core extraction source selection.
|
||||
pub use crate::core_extraction::CoreExtractionSource;
|
||||
/// Final core extraction counters.
|
||||
pub use crate::core_extraction::CoreExtractionSummary;
|
||||
/// Executes one bounded canonical transaction to core extraction campaign.
|
||||
pub use crate::core_extraction::execute_core_extraction;
|
||||
/// Extracts one canonical raw transaction into a complete core write bundle.
|
||||
pub use crate::core_extraction::extract_raw_transaction_to_core;
|
||||
/// Current common decode pipeline orchestration version.
|
||||
pub use crate::decode_replay::DECODE_PIPELINE_VERSION;
|
||||
/// Deterministic policy used when several decoders recognize one input.
|
||||
pub use crate::decode_replay::DecodeDispatchPolicy;
|
||||
/// Aggregated terminal counters for one decoder version.
|
||||
pub use crate::decode_replay::DecodeProcessorSummary;
|
||||
/// Progress and cooperative cancellation contract implemented by applications.
|
||||
pub use crate::decode_replay::DecodeReplayObserver;
|
||||
/// One operator-visible decode replay progress event.
|
||||
pub use crate::decode_replay::DecodeReplayProgressEvent;
|
||||
/// Decode replay progress severity.
|
||||
pub use crate::decode_replay::DecodeReplayProgressLevel;
|
||||
/// Complete bounded contextual decode replay request.
|
||||
pub use crate::decode_replay::DecodeReplayRequest;
|
||||
/// Final counters for one bounded contextual decode replay campaign.
|
||||
pub use crate::decode_replay::DecodeReplaySummary;
|
||||
/// Stable processing ledger stage used by decoded event materializers.
|
||||
pub use crate::decode_replay::EVENT_MATERIALIZATION_STAGE;
|
||||
/// Stable processing ledger stage used by contextual instruction decoders.
|
||||
pub use crate::decode_replay::INSTRUCTION_DECODE_STAGE;
|
||||
/// Executes one bounded contextual instruction decode and optional materialization campaign.
|
||||
pub use crate::decode_replay::execute_decode_replay;
|
||||
/// Creates one stable process-local contextual decode campaign identifier.
|
||||
pub use crate::decode_replay::new_decode_campaign_id;
|
||||
/// Pipeline stage identifier.
|
||||
pub use crate::plan::PipelineStage;
|
||||
/// Replay selection scope.
|
||||
pub use crate::plan::ReplayScope;
|
||||
/// Complete request for one Devnet ATA execution.
|
||||
pub use crate::solana_ata_execution::DevnetSplAssociatedTokenAccountExecutionRequest;
|
||||
/// Complete result of one Devnet ATA execution.
|
||||
pub use crate::solana_ata_execution::DevnetSplAssociatedTokenAccountExecutionSummary;
|
||||
/// Executes one Devnet ATA simulation or explicitly authorized submission.
|
||||
pub use crate::solana_ata_execution::execute_devnet_spl_associated_token_account;
|
||||
/// Simulates one Devnet ATA operation after stateful preflight.
|
||||
pub use crate::solana_ata_execution::simulate_devnet_spl_associated_token_account;
|
||||
/// Stateful ATA invariants observed after a confirmed execution.
|
||||
pub use crate::solana_ata_stateful::SplAssociatedTokenAccountPostExecutionReport;
|
||||
/// One ATA stateful readiness check.
|
||||
pub use crate::solana_ata_stateful::SplAssociatedTokenAccountStatefulCheck;
|
||||
/// One ATA stateful readiness fact.
|
||||
pub use crate::solana_ata_stateful::SplAssociatedTokenAccountStatefulFact;
|
||||
/// Complete ATA stateful readiness report.
|
||||
pub use crate::solana_ata_stateful::SplAssociatedTokenAccountStatefulReadinessReport;
|
||||
/// Complete ATA stateful readiness request.
|
||||
pub use crate::solana_ata_stateful::SplAssociatedTokenAccountStatefulReadinessRequest;
|
||||
/// ATA stateful readiness outcome.
|
||||
pub use crate::solana_ata_stateful::SplAssociatedTokenAccountStatefulReadinessStatus;
|
||||
/// Verifies final ATA account relationships after a confirmed execution.
|
||||
pub use crate::solana_ata_stateful::inspect_spl_associated_token_account_post_execution;
|
||||
/// Inspects Localnet or Devnet ATA state required before simulation.
|
||||
pub use crate::solana_ata_stateful::inspect_spl_associated_token_account_stateful_readiness;
|
||||
/// Exact byte length of one SPL ElGamal registry account.
|
||||
pub use crate::solana_elgamal_registry_stateful::ELGAMAL_REGISTRY_STATEFUL_ACCOUNT_BYTES;
|
||||
/// One bounded SPL ElGamal registry RPC read request.
|
||||
pub use crate::solana_elgamal_registry_stateful::ElGamalRegistryStatefulReadRequest;
|
||||
/// One bounded registry RPC read and validated administrative projection.
|
||||
pub use crate::solana_elgamal_registry_stateful::ElGamalRegistryStatefulReadResult;
|
||||
/// One contextually validated SPL ElGamal registry state snapshot.
|
||||
pub use crate::solana_elgamal_registry_stateful::ElGamalRegistryStatefulSnapshot;
|
||||
/// Validates one complete RPC registry response before parsing and routing.
|
||||
pub use crate::solana_elgamal_registry_stateful::materialize_elgamal_registry_account_info_result;
|
||||
/// Parses, validates the official owner-derived PDA, and materializes one registry snapshot.
|
||||
pub use crate::solana_elgamal_registry_stateful::materialize_elgamal_registry_stateful_snapshot;
|
||||
/// Reads, validates, and routes one exact SPL ElGamal registry account.
|
||||
pub use crate::solana_elgamal_registry_stateful::read_elgamal_registry_stateful_snapshot;
|
||||
/// Complete request for one bounded Devnet System transfer.
|
||||
pub use crate::solana_execution::DevnetSystemTransferRequest;
|
||||
/// Complete result of one Devnet System transfer and post-validation.
|
||||
pub use crate::solana_execution::DevnetSystemTransferSummary;
|
||||
/// No-op observer for execution orchestration.
|
||||
pub use crate::solana_execution::NoopSolanaExecutionObserver;
|
||||
/// Composite execution, backfill, extraction and decode observer.
|
||||
pub use crate::solana_execution::SolanaExecutionObserver;
|
||||
/// One operator-visible execution progress event.
|
||||
pub use crate::solana_execution::SolanaExecutionProgressEvent;
|
||||
/// Execution progress severity.
|
||||
pub use crate::solana_execution::SolanaExecutionProgressLevel;
|
||||
/// Executes one bounded Devnet System transfer and canonical replay validation.
|
||||
pub use crate::solana_execution::execute_devnet_system_transfer;
|
||||
/// Complete request for one SPL Memo v4 Devnet execution.
|
||||
pub use crate::solana_memo_execution::DevnetMemoExecutionRequest;
|
||||
/// Complete result of one SPL Memo v4 Devnet execution and post-validation.
|
||||
pub use crate::solana_memo_execution::DevnetMemoExecutionSummary;
|
||||
/// Executes one SPL Memo v4 Devnet simulation or explicitly authorized submission.
|
||||
pub use crate::solana_memo_execution::execute_devnet_memo;
|
||||
/// One machine-readable stateful readiness check.
|
||||
pub use crate::solana_stateful::SolanaCoreStatefulCheck;
|
||||
/// One contextual fact measured during stateful readiness inspection.
|
||||
pub use crate::solana_stateful::SolanaCoreStatefulFact;
|
||||
/// Complete stateful readiness report produced before simulation.
|
||||
pub use crate::solana_stateful::SolanaCoreStatefulReadinessReport;
|
||||
/// Complete request for one native Solana stateful readiness inspection.
|
||||
pub use crate::solana_stateful::SolanaCoreStatefulReadinessRequest;
|
||||
/// Stateful readiness outcome for one native Solana operation.
|
||||
pub use crate::solana_stateful::SolanaCoreStatefulReadinessStatus;
|
||||
/// Inspects Localnet or Devnet state required before simulation.
|
||||
pub use crate::solana_stateful::inspect_solana_core_stateful_readiness;
|
||||
/// Correlation outcome between one committed instruction fact and one final state snapshot.
|
||||
pub use crate::solana_token_2022_correlation::Token2022CorrelationStatus;
|
||||
/// Deterministic correlation report for one instruction output and one final snapshot.
|
||||
pub use crate::solana_token_2022_correlation::Token2022StateCorrelationReport;
|
||||
/// Correlates one materialized instruction fact with one authoritative Token-2022 snapshot.
|
||||
pub use crate::solana_token_2022_correlation::correlate_token_2022_instruction_with_snapshot;
|
||||
/// Maximum proof context-state accounts accepted by one Token-2022 cryptographic preflight.
|
||||
pub use crate::solana_token_2022_crypto_preflight::MAX_TOKEN_2022_PROOF_CONTEXTS;
|
||||
/// Complete Token-2022 cryptographic preflight report.
|
||||
pub use crate::solana_token_2022_crypto_preflight::Token2022CryptographicPreflightReport;
|
||||
/// One bounded Token-2022 cryptographic preflight request.
|
||||
pub use crate::solana_token_2022_crypto_preflight::Token2022CryptographicPreflightRequest;
|
||||
/// One validated proof context-state result.
|
||||
pub use crate::solana_token_2022_crypto_preflight::Token2022ProofContextReport;
|
||||
/// One exact pre-verified proof context-state requirement.
|
||||
pub use crate::solana_token_2022_crypto_preflight::Token2022ProofContextRequirement;
|
||||
/// Generic metadata bytes retained by every ZK proof context-state account.
|
||||
pub use crate::solana_token_2022_crypto_preflight::ZK_PROOF_CONTEXT_META_BYTES;
|
||||
/// Reads and validates pre-verified proof context-state accounts before simulation.
|
||||
pub use crate::solana_token_2022_crypto_preflight::inspect_token_2022_cryptographic_preflight;
|
||||
/// Complete request for one public Devnet Token-2022 execution.
|
||||
pub use crate::solana_token_2022_devnet_execution::DevnetSplToken2022ExecutionRequest;
|
||||
/// Complete result of one public Devnet Token-2022 execution.
|
||||
pub use crate::solana_token_2022_devnet_execution::DevnetSplToken2022ExecutionSummary;
|
||||
/// Executes one public Devnet Token-2022 simulation or authorized submission.
|
||||
pub use crate::solana_token_2022_devnet_execution::execute_devnet_spl_token_2022;
|
||||
/// Simulates one public Devnet Token-2022 operation.
|
||||
pub use crate::solana_token_2022_devnet_execution::simulate_devnet_spl_token_2022;
|
||||
/// Stable family of one Devnet SPL validation scenario.
|
||||
pub use crate::solana_token_2022_devnet_scenarios::DevnetSplValidationFamily;
|
||||
/// Current implementation status of one Devnet SPL validation scenario.
|
||||
pub use crate::solana_token_2022_devnet_scenarios::DevnetSplValidationImplementationStatus;
|
||||
/// One independent Devnet validation scenario required by milestone 0.4.6.
|
||||
pub use crate::solana_token_2022_devnet_scenarios::DevnetSplValidationScenario;
|
||||
/// Returns the ordered Devnet SPL scenario inventory required by milestone 0.4.6.
|
||||
pub use crate::solana_token_2022_devnet_scenarios::devnet_spl_validation_scenarios;
|
||||
/// Maximum distinct signers accepted by one Token-2022 execution envelope.
|
||||
pub use crate::solana_token_2022_execution_orchestration::MAX_TOKEN_2022_EXECUTION_SIGNERS;
|
||||
/// One explicit stateful postcondition retained after confirmation.
|
||||
pub use crate::solana_token_2022_execution_orchestration::Token2022ExecutionPostcondition;
|
||||
/// Explicit result of one Token-2022 stateful postcondition.
|
||||
pub use crate::solana_token_2022_execution_orchestration::Token2022ExecutionPostconditionStatus;
|
||||
/// Deterministic report authorizing or refusing Token-2022 signing and submission.
|
||||
pub use crate::solana_token_2022_execution_orchestration::Token2022ExecutionReadinessReport;
|
||||
/// Complete request checked before Token-2022 transaction signing.
|
||||
pub use crate::solana_token_2022_execution_orchestration::Token2022ExecutionReadinessRequest;
|
||||
/// Aggregates explicit postconditions without inventing success.
|
||||
pub use crate::solana_token_2022_execution_orchestration::summarize_token_2022_postconditions;
|
||||
/// Validates exact-message simulation, preflights, signers, and submission policy.
|
||||
pub use crate::solana_token_2022_execution_orchestration::validate_token_2022_execution_readiness;
|
||||
/// Maximum distinct Token-2022 accounts accepted by one preflight inspection.
|
||||
pub use crate::solana_token_2022_preflight::MAX_TOKEN_2022_PREFLIGHT_ACCOUNTS;
|
||||
/// Maximum aggregate account-data budget accepted by one preflight inspection.
|
||||
pub use crate::solana_token_2022_preflight::MAX_TOKEN_2022_PREFLIGHT_TOTAL_BYTES;
|
||||
/// One validated account result in a Token-2022 preflight report.
|
||||
pub use crate::solana_token_2022_preflight::Token2022PreflightAccountReport;
|
||||
/// Complete bounded Token-2022 stateful preflight report.
|
||||
pub use crate::solana_token_2022_preflight::Token2022PreflightReport;
|
||||
/// One bounded Token-2022 preflight request.
|
||||
pub use crate::solana_token_2022_preflight::Token2022PreflightRequest;
|
||||
/// One exact Token-2022 account requirement for a stateful preflight.
|
||||
pub use crate::solana_token_2022_preflight::Token2022PreflightRequirement;
|
||||
/// Inspects all bounded Token-2022 state required before simulation.
|
||||
pub use crate::solana_token_2022_preflight::inspect_token_2022_preflight;
|
||||
/// Maximum proof references accepted by one confidential Token-2022 operation.
|
||||
pub use crate::solana_token_2022_proof_orchestration::MAX_TOKEN_2022_OPERATION_PROOFS;
|
||||
/// One deterministic mixed-proof orchestration report.
|
||||
pub use crate::solana_token_2022_proof_orchestration::Token2022ProofOrchestrationReport;
|
||||
/// One bounded mixed-proof orchestration request.
|
||||
pub use crate::solana_token_2022_proof_orchestration::Token2022ProofOrchestrationRequest;
|
||||
/// Validates mixed inline/context-state proofs before transaction assembly.
|
||||
pub use crate::solana_token_2022_proof_orchestration::orchestrate_token_2022_proofs;
|
||||
/// Maximum complete Token-2022 account data accepted by one bounded RPC read.
|
||||
pub use crate::solana_token_2022_stateful::MAX_TOKEN_2022_STATEFUL_ACCOUNT_BYTES;
|
||||
/// Optional external identities required to validate cross-account Token-2022 state.
|
||||
pub use crate::solana_token_2022_stateful::Token2022StatefulContext;
|
||||
/// One bounded Token-2022 account read request.
|
||||
pub use crate::solana_token_2022_stateful::Token2022StatefulReadRequest;
|
||||
/// One bounded RPC read and its contextually validated projections.
|
||||
pub use crate::solana_token_2022_stateful::Token2022StatefulReadResult;
|
||||
/// One contextually validated Token-2022 state snapshot and its owned projections.
|
||||
pub use crate::solana_token_2022_stateful::Token2022StatefulSnapshotBundle;
|
||||
/// Contextually validates and materializes one already parsed Token-2022 account snapshot.
|
||||
pub use crate::solana_token_2022_stateful::materialize_parsed_token_2022_stateful_snapshot;
|
||||
/// Contextually validates one parsed Token-2022 snapshot with external cross-account identities.
|
||||
pub use crate::solana_token_2022_stateful::materialize_parsed_token_2022_stateful_snapshot_with_context;
|
||||
/// Validates one complete RPC account response before Token-2022 parsing and routing.
|
||||
pub use crate::solana_token_2022_stateful::materialize_token_2022_account_info_result;
|
||||
/// Parses, contextually validates, and materializes one bounded Token-2022 account snapshot.
|
||||
pub use crate::solana_token_2022_stateful::materialize_token_2022_stateful_snapshot;
|
||||
/// Reads, validates, parses, and routes one bounded Token-2022 account snapshot.
|
||||
pub use crate::solana_token_2022_stateful::read_token_2022_stateful_snapshot;
|
||||
/// Maximum evidence records retained per Token-2022 validation scenario.
|
||||
pub use crate::solana_token_2022_validation::MAX_TOKEN_2022_VALIDATION_EVIDENCE;
|
||||
/// Required environment for one Token-2022 validation scenario.
|
||||
pub use crate::solana_token_2022_validation::Token2022ValidationEnvironment;
|
||||
/// One bounded validation evidence record.
|
||||
pub use crate::solana_token_2022_validation::Token2022ValidationEvidence;
|
||||
/// Canonical machine-readable Token-2022 validation matrix.
|
||||
pub use crate::solana_token_2022_validation::Token2022ValidationMatrix;
|
||||
/// One scenario declared in the canonical Token-2022 validation matrix.
|
||||
pub use crate::solana_token_2022_validation::Token2022ValidationMatrixScenario;
|
||||
/// Complete Token-2022 milestone validation report.
|
||||
pub use crate::solana_token_2022_validation::Token2022ValidationReport;
|
||||
/// One declared Token-2022 validation scenario.
|
||||
pub use crate::solana_token_2022_validation::Token2022ValidationScenario;
|
||||
/// Exact observed status of one Token-2022 validation scenario.
|
||||
pub use crate::solana_token_2022_validation::Token2022ValidationStatus;
|
||||
/// Loads and validates the canonical Token-2022 validation matrix.
|
||||
pub use crate::solana_token_2022_validation::load_token_2022_validation_matrix;
|
||||
/// Validates the canonical Token-2022 validation matrix contract and evidence.
|
||||
pub use crate::solana_token_2022_validation::validate_token_2022_validation_matrix;
|
||||
/// Validates a bounded Token-2022 milestone evidence report.
|
||||
pub use crate::solana_token_2022_validation::validate_token_2022_validation_report;
|
||||
/// Complete request for one Devnet classic SPL Token execution.
|
||||
pub use crate::solana_token_execution::DevnetSplTokenExecutionRequest;
|
||||
/// Complete result of one Devnet classic SPL Token execution.
|
||||
pub use crate::solana_token_execution::DevnetSplTokenExecutionSummary;
|
||||
/// Executes one Devnet classic SPL Token simulation or authorized submission.
|
||||
pub use crate::solana_token_execution::execute_devnet_spl_token;
|
||||
/// Simulates one Devnet classic SPL Token operation after stateful preflight.
|
||||
pub use crate::solana_token_execution::simulate_devnet_spl_token;
|
||||
/// Explicit request to prepare fresh raw accounts for one SPL Token Devnet lifecycle.
|
||||
pub use crate::solana_token_lifecycle::DevnetSplTokenLifecyclePreparationRequest;
|
||||
/// One confirmed raw account creation retained by lifecycle preparation.
|
||||
pub use crate::solana_token_lifecycle::DevnetSplTokenLifecyclePreparationStep;
|
||||
/// Complete fresh-account preparation result for one SPL Token Devnet lifecycle.
|
||||
pub use crate::solana_token_lifecycle::DevnetSplTokenLifecyclePreparationSummary;
|
||||
/// Request for one destructive, explicitly authorized SPL Token Devnet lifecycle.
|
||||
pub use crate::solana_token_lifecycle::DevnetSplTokenLifecycleRequest;
|
||||
/// Result retained for one fully post-validated lifecycle transaction.
|
||||
pub use crate::solana_token_lifecycle::DevnetSplTokenLifecycleStepSummary;
|
||||
/// Complete result of the controlled SPL Token Devnet lifecycle.
|
||||
pub use crate::solana_token_lifecycle::DevnetSplTokenLifecycleSummary;
|
||||
/// Executes initialize, mint, transfer, approve/revoke, burn and close on Devnet.
|
||||
pub use crate::solana_token_lifecycle::execute_devnet_spl_token_lifecycle;
|
||||
/// Creates and verifies fresh raw Token-owned accounts for one Devnet lifecycle.
|
||||
pub use crate::solana_token_lifecycle::prepare_devnet_spl_token_lifecycle_accounts;
|
||||
/// One machine-readable classic SPL Token stateful check.
|
||||
pub use crate::solana_token_stateful::SplTokenStatefulCheck;
|
||||
/// One contextual fact measured during classic SPL Token state inspection.
|
||||
pub use crate::solana_token_stateful::SplTokenStatefulFact;
|
||||
/// Complete classic SPL Token stateful readiness report.
|
||||
pub use crate::solana_token_stateful::SplTokenStatefulReadinessReport;
|
||||
/// Complete request for one classic SPL Token stateful readiness inspection.
|
||||
pub use crate::solana_token_stateful::SplTokenStatefulReadinessRequest;
|
||||
/// Stateful readiness outcome for one classic SPL Token operation.
|
||||
pub use crate::solana_token_stateful::SplTokenStatefulReadinessStatus;
|
||||
/// Inspects Localnet or Devnet Token state before simulation.
|
||||
pub use crate::solana_token_stateful::inspect_spl_token_stateful_readiness;
|
||||
44
migration/khadhroony-bot2-reference/kb_pipeline/src/plan.rs
Normal file
44
migration/khadhroony-bot2-reference/kb_pipeline/src/plan.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
// file: kb_pipeline/src/plan.rs
|
||||
// version: 1
|
||||
|
||||
//! Pipeline planning primitives.
|
||||
|
||||
/// Pipeline stage identifier.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum PipelineStage {
|
||||
/// Ingest raw transactions.
|
||||
Ingest,
|
||||
/// Extract generic Solana structures.
|
||||
Extract,
|
||||
/// Build program observations.
|
||||
Observe,
|
||||
/// Decode protocol events.
|
||||
Decode,
|
||||
/// Materialize business events.
|
||||
Materialize,
|
||||
/// Aggregate materialized events.
|
||||
Aggregate,
|
||||
/// Validate outputs.
|
||||
Validate,
|
||||
}
|
||||
|
||||
/// Replay selection scope.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ReplayScope {
|
||||
/// Optional module name.
|
||||
pub module_name: std::option::Option<std::string::String>,
|
||||
/// Optional module version.
|
||||
pub module_version: std::option::Option<std::string::String>,
|
||||
/// Optional program id.
|
||||
pub program_id: std::option::Option<std::string::String>,
|
||||
/// Optional surface code.
|
||||
pub surface_code: std::option::Option<std::string::String>,
|
||||
/// Optional 8-byte discriminator in hexadecimal.
|
||||
pub discriminator_8: std::option::Option<std::string::String>,
|
||||
/// Optional inclusive start slot.
|
||||
pub slot_start: std::option::Option<u64>,
|
||||
/// Optional inclusive end slot.
|
||||
pub slot_end: std::option::Option<u64>,
|
||||
/// Whether to ignore existing ledger rows.
|
||||
pub force: bool,
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,406 @@
|
||||
// file: kb_pipeline/src/solana_elgamal_registry_stateful.rs
|
||||
// version: 3
|
||||
|
||||
//! Contextual SPL ElGamal registry account-state validation and materialization routing.
|
||||
|
||||
use std::str::FromStr; // rust-rules: trait-import
|
||||
|
||||
/// Exact byte length of one SPL ElGamal registry account.
|
||||
pub const ELGAMAL_REGISTRY_STATEFUL_ACCOUNT_BYTES: usize = 64;
|
||||
|
||||
/// One bounded SPL ElGamal registry RPC read request.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ElGamalRegistryStatefulReadRequest {
|
||||
/// Endpoint role used for the HTTP RPC request.
|
||||
pub query_role: std::string::String,
|
||||
/// Canonical registry PDA.
|
||||
pub registry_account: kb_model::Pubkey,
|
||||
/// Optional minimum RPC context slot.
|
||||
pub min_context_slot: std::option::Option<u64>,
|
||||
}
|
||||
|
||||
/// One bounded registry RPC read and validated administrative projection.
|
||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct ElGamalRegistryStatefulReadResult {
|
||||
/// Commitment used for the RPC read.
|
||||
pub commitment: std::string::String,
|
||||
/// Context slot returned by the endpoint.
|
||||
pub context_slot: u64,
|
||||
/// Complete validated registry snapshot.
|
||||
pub snapshot: crate::solana_elgamal_registry_stateful::ElGamalRegistryStatefulSnapshot,
|
||||
}
|
||||
|
||||
/// Reads, validates, and routes one exact SPL ElGamal registry account.
|
||||
pub async fn read_elgamal_registry_stateful_snapshot(
|
||||
pool: &kb_rpc::HttpEndpointPool,
|
||||
request: &crate::solana_elgamal_registry_stateful::ElGamalRegistryStatefulReadRequest,
|
||||
) -> kb_core::Result<crate::solana_elgamal_registry_stateful::ElGamalRegistryStatefulReadResult> {
|
||||
if request.query_role.trim().is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"SPL ElGamal registry stateful read query_role must not be empty",
|
||||
));
|
||||
}
|
||||
let config = match kb_rpc::GetAccountInfoConfig::new_with_data(
|
||||
kb_rpc::RpcCommitmentLevel::Confirmed,
|
||||
request.min_context_slot,
|
||||
crate::solana_elgamal_registry_stateful::ELGAMAL_REGISTRY_STATEFUL_ACCOUNT_BYTES,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let result = match pool
|
||||
.get_account_info_for_role(request.query_role.as_str(), &request.registry_account, &config)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return crate::solana_elgamal_registry_stateful::materialize_elgamal_registry_account_info_result(
|
||||
request,
|
||||
&result,
|
||||
);
|
||||
}
|
||||
|
||||
/// Validates one complete RPC registry response before parsing and routing.
|
||||
pub fn materialize_elgamal_registry_account_info_result(
|
||||
request: &crate::solana_elgamal_registry_stateful::ElGamalRegistryStatefulReadRequest,
|
||||
result: &kb_rpc::AccountInfoResult,
|
||||
) -> kb_core::Result<crate::solana_elgamal_registry_stateful::ElGamalRegistryStatefulReadResult> {
|
||||
if let std::option::Option::Some(min_context_slot) = request.min_context_slot {
|
||||
if result.context.slot < min_context_slot {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"elgamal_registry_stateful_context_slot_too_old",
|
||||
format!(
|
||||
"SPL ElGamal registry context slot {} is below requested minimum {min_context_slot}",
|
||||
result.context.slot
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
let account = match result.account.as_ref() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"elgamal_registry_stateful_account_missing",
|
||||
format!(
|
||||
"SPL ElGamal registry account {} does not exist",
|
||||
request.registry_account.0
|
||||
),
|
||||
));
|
||||
},
|
||||
};
|
||||
if account.executable {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"elgamal_registry_stateful_account_executable",
|
||||
format!(
|
||||
"SPL ElGamal registry state account {} must not be executable",
|
||||
request.registry_account.0
|
||||
),
|
||||
));
|
||||
}
|
||||
if account.owner.0 != kb_program_ids::SPL_TOKEN_2022_ELGAMAL_REGISTRY_PROGRAM_ID {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"elgamal_registry_stateful_owner_mismatch",
|
||||
format!(
|
||||
"SPL ElGamal registry owner must be {}, got {}",
|
||||
kb_program_ids::SPL_TOKEN_2022_ELGAMAL_REGISTRY_PROGRAM_ID,
|
||||
account.owner.0
|
||||
),
|
||||
));
|
||||
}
|
||||
if account.space
|
||||
!= crate::solana_elgamal_registry_stateful::ELGAMAL_REGISTRY_STATEFUL_ACCOUNT_BYTES as u64
|
||||
|| account.data.len()
|
||||
!= crate::solana_elgamal_registry_stateful::ELGAMAL_REGISTRY_STATEFUL_ACCOUNT_BYTES
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"elgamal_registry_stateful_account_length_invalid",
|
||||
format!(
|
||||
"SPL ElGamal registry account must report and return exactly {} bytes, got space {} and data {}",
|
||||
crate::solana_elgamal_registry_stateful::ELGAMAL_REGISTRY_STATEFUL_ACCOUNT_BYTES,
|
||||
account.space,
|
||||
account.data.len()
|
||||
),
|
||||
));
|
||||
}
|
||||
let snapshot = match crate::solana_elgamal_registry_stateful::materialize_elgamal_registry_stateful_snapshot(
|
||||
request.registry_account.0.as_str(),
|
||||
account.owner.0.as_str(),
|
||||
result.context.slot,
|
||||
account.data.as_slice(),
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"elgamal_registry_stateful_projection_failed",
|
||||
error,
|
||||
));
|
||||
},
|
||||
};
|
||||
return std::result::Result::Ok(
|
||||
crate::solana_elgamal_registry_stateful::ElGamalRegistryStatefulReadResult {
|
||||
commitment: "confirmed".to_string(),
|
||||
context_slot: result.context.slot,
|
||||
snapshot,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// One contextually validated SPL ElGamal registry state snapshot.
|
||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct ElGamalRegistryStatefulSnapshot {
|
||||
/// Canonical registry PDA identity.
|
||||
pub registry_account_key: std::string::String,
|
||||
/// Wallet address stored as the registry owner.
|
||||
pub owner: std::string::String,
|
||||
/// Context slot associated with the account read.
|
||||
pub slot: u64,
|
||||
/// Processor-owned administrative projection.
|
||||
pub output: kb_materializer_api::MaterializedOutput,
|
||||
}
|
||||
|
||||
/// Parse, validate the official owner-derived PDA, and materialize one registry snapshot.
|
||||
pub fn materialize_elgamal_registry_stateful_snapshot(
|
||||
registry_account_key: &str,
|
||||
owner_program_id: &str,
|
||||
slot: u64,
|
||||
data: &[u8],
|
||||
) -> std::result::Result<
|
||||
crate::solana_elgamal_registry_stateful::ElGamalRegistryStatefulSnapshot,
|
||||
String,
|
||||
> {
|
||||
if owner_program_id != kb_program_ids::SPL_TOKEN_2022_ELGAMAL_REGISTRY_PROGRAM_ID {
|
||||
return std::result::Result::Err(format!(
|
||||
"SPL ElGamal registry snapshot owner must be {}, got {owner_program_id}",
|
||||
kb_program_ids::SPL_TOKEN_2022_ELGAMAL_REGISTRY_PROGRAM_ID
|
||||
));
|
||||
}
|
||||
let state = match kb_decoder_spl_elgamal_registry::state::parse_elgamal_registry_state(data) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let owner = match solana_pubkey::Pubkey::from_str(state.owner.as_str()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(format!(
|
||||
"SPL ElGamal registry owner is not a valid address: {error}"
|
||||
));
|
||||
},
|
||||
};
|
||||
let program_id = match solana_pubkey::Pubkey::from_str(
|
||||
kb_program_ids::SPL_TOKEN_2022_ELGAMAL_REGISTRY_PROGRAM_ID,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(format!(
|
||||
"Configured SPL ElGamal registry program ID is invalid: {error}"
|
||||
));
|
||||
},
|
||||
};
|
||||
let expected =
|
||||
spl_elgamal_registry_interface::get_elgamal_registry_address(&owner, &program_id);
|
||||
if expected.to_string() != registry_account_key {
|
||||
return std::result::Result::Err(format!(
|
||||
"SPL ElGamal registry account {registry_account_key} does not match owner-derived PDA {expected}"
|
||||
));
|
||||
}
|
||||
let output = match kb_materializer_admin::materialize_elgamal_registry_state_snapshot(
|
||||
registry_account_key,
|
||||
slot,
|
||||
&state,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(
|
||||
crate::solana_elgamal_registry_stateful::ElGamalRegistryStatefulSnapshot {
|
||||
registry_account_key: registry_account_key.to_string(),
|
||||
owner: state.owner,
|
||||
slot,
|
||||
output,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::str::FromStr; // rust-rules: trait-import
|
||||
|
||||
fn registry_fixture(owner: &solana_pubkey::Pubkey) -> (std::string::String, [u8; 64]) {
|
||||
let program_id = match solana_pubkey::Pubkey::from_str(
|
||||
kb_program_ids::SPL_TOKEN_2022_ELGAMAL_REGISTRY_PROGRAM_ID,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => solana_pubkey::Pubkey::default(),
|
||||
};
|
||||
let registry =
|
||||
spl_elgamal_registry_interface::get_elgamal_registry_address(owner, &program_id);
|
||||
let mut data = [0u8; 64];
|
||||
data[..32].copy_from_slice(owner.as_ref());
|
||||
data[32..].copy_from_slice(&[9u8; 32]);
|
||||
return (registry.to_string(), data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_owner_derived_registry_pda_routes_one_admin_snapshot() {
|
||||
let owner = solana_pubkey::Pubkey::new_from_array([7u8; 32]);
|
||||
let (registry, data) =
|
||||
crate::solana_elgamal_registry_stateful::tests::registry_fixture(&owner);
|
||||
let snapshot =
|
||||
crate::solana_elgamal_registry_stateful::materialize_elgamal_registry_stateful_snapshot(
|
||||
registry.as_str(),
|
||||
kb_program_ids::SPL_TOKEN_2022_ELGAMAL_REGISTRY_PROGRAM_ID,
|
||||
55,
|
||||
&data,
|
||||
);
|
||||
assert_eq!(
|
||||
snapshot.as_ref().map(|value| return value.owner.clone()),
|
||||
std::result::Result::Ok(owner.to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
snapshot
|
||||
.as_ref()
|
||||
.map(|value| return value.output.payload_json["finalAccountStateCaptured"].clone()),
|
||||
std::result::Result::Ok(serde_json::json!(true))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn foreign_owner_program_wrong_pda_and_invalid_length_fail_closed() {
|
||||
let owner = solana_pubkey::Pubkey::new_from_array([8u8; 32]);
|
||||
let (registry, data) =
|
||||
crate::solana_elgamal_registry_stateful::tests::registry_fixture(&owner);
|
||||
let foreign =
|
||||
crate::solana_elgamal_registry_stateful::materialize_elgamal_registry_stateful_snapshot(
|
||||
registry.as_str(),
|
||||
kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID,
|
||||
55,
|
||||
&data,
|
||||
);
|
||||
assert!(foreign.is_err());
|
||||
let wrong =
|
||||
crate::solana_elgamal_registry_stateful::materialize_elgamal_registry_stateful_snapshot(
|
||||
solana_pubkey::Pubkey::new_from_array([10u8; 32]).to_string().as_str(),
|
||||
kb_program_ids::SPL_TOKEN_2022_ELGAMAL_REGISTRY_PROGRAM_ID,
|
||||
55,
|
||||
&data,
|
||||
);
|
||||
assert!(wrong.is_err());
|
||||
let invalid =
|
||||
crate::solana_elgamal_registry_stateful::materialize_elgamal_registry_stateful_snapshot(
|
||||
registry.as_str(),
|
||||
kb_program_ids::SPL_TOKEN_2022_ELGAMAL_REGISTRY_PROGRAM_ID,
|
||||
55,
|
||||
&data[..63],
|
||||
);
|
||||
assert!(invalid.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_rpc_registry_routes_after_context_owner_and_length_validation() {
|
||||
let owner = solana_pubkey::Pubkey::new_from_array([13u8; 32]);
|
||||
let (registry, data) =
|
||||
crate::solana_elgamal_registry_stateful::tests::registry_fixture(&owner);
|
||||
let request = crate::solana_elgamal_registry_stateful::ElGamalRegistryStatefulReadRequest {
|
||||
query_role: "execution".to_string(),
|
||||
registry_account: kb_model::Pubkey(registry),
|
||||
min_context_slot: std::option::Option::Some(70),
|
||||
};
|
||||
let result = kb_rpc::AccountInfoResult {
|
||||
context: kb_rpc::RpcResponseContext {
|
||||
slot: 71,
|
||||
api_version: std::option::Option::None,
|
||||
},
|
||||
account: std::option::Option::Some(kb_rpc::AccountInfoValue {
|
||||
lamports: 1,
|
||||
owner: kb_model::ProgramId(
|
||||
kb_program_ids::SPL_TOKEN_2022_ELGAMAL_REGISTRY_PROGRAM_ID.to_string(),
|
||||
),
|
||||
executable: false,
|
||||
rent_epoch: 0,
|
||||
space: 64,
|
||||
data: data.to_vec(),
|
||||
}),
|
||||
};
|
||||
let materialized = crate::solana_elgamal_registry_stateful::materialize_elgamal_registry_account_info_result(
|
||||
&request,
|
||||
&result,
|
||||
);
|
||||
assert_eq!(
|
||||
materialized.as_ref().map(|value| return value.context_slot),
|
||||
std::result::Result::Ok(71)
|
||||
);
|
||||
assert_eq!(
|
||||
materialized.as_ref().map(|value| return value.snapshot.owner.clone()),
|
||||
std::result::Result::Ok(owner.to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stale_missing_foreign_executable_and_wrong_length_registry_reads_fail_closed() {
|
||||
let owner = solana_pubkey::Pubkey::new_from_array([14u8; 32]);
|
||||
let (registry, data) =
|
||||
crate::solana_elgamal_registry_stateful::tests::registry_fixture(&owner);
|
||||
let request = crate::solana_elgamal_registry_stateful::ElGamalRegistryStatefulReadRequest {
|
||||
query_role: "execution".to_string(),
|
||||
registry_account: kb_model::Pubkey(registry),
|
||||
min_context_slot: std::option::Option::Some(80),
|
||||
};
|
||||
let base = kb_rpc::AccountInfoValue {
|
||||
lamports: 1,
|
||||
owner: kb_model::ProgramId(
|
||||
kb_program_ids::SPL_TOKEN_2022_ELGAMAL_REGISTRY_PROGRAM_ID.to_string(),
|
||||
),
|
||||
executable: false,
|
||||
rent_epoch: 0,
|
||||
space: 64,
|
||||
data: data.to_vec(),
|
||||
};
|
||||
let stale = kb_rpc::AccountInfoResult {
|
||||
context: kb_rpc::RpcResponseContext {
|
||||
slot: 79,
|
||||
api_version: std::option::Option::None,
|
||||
},
|
||||
account: std::option::Option::Some(base.clone()),
|
||||
};
|
||||
assert!(crate::solana_elgamal_registry_stateful::materialize_elgamal_registry_account_info_result(&request, &stale).is_err());
|
||||
let missing = kb_rpc::AccountInfoResult {
|
||||
context: kb_rpc::RpcResponseContext {
|
||||
slot: 80,
|
||||
api_version: std::option::Option::None,
|
||||
},
|
||||
account: std::option::Option::None,
|
||||
};
|
||||
assert!(crate::solana_elgamal_registry_stateful::materialize_elgamal_registry_account_info_result(&request, &missing).is_err());
|
||||
let mut foreign = base.clone();
|
||||
foreign.owner = kb_model::ProgramId(kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID.to_string());
|
||||
let foreign = kb_rpc::AccountInfoResult {
|
||||
context: kb_rpc::RpcResponseContext {
|
||||
slot: 80,
|
||||
api_version: std::option::Option::None,
|
||||
},
|
||||
account: std::option::Option::Some(foreign),
|
||||
};
|
||||
assert!(crate::solana_elgamal_registry_stateful::materialize_elgamal_registry_account_info_result(&request, &foreign).is_err());
|
||||
let mut executable = base.clone();
|
||||
executable.executable = true;
|
||||
let executable = kb_rpc::AccountInfoResult {
|
||||
context: kb_rpc::RpcResponseContext {
|
||||
slot: 80,
|
||||
api_version: std::option::Option::None,
|
||||
},
|
||||
account: std::option::Option::Some(executable),
|
||||
};
|
||||
assert!(crate::solana_elgamal_registry_stateful::materialize_elgamal_registry_account_info_result(&request, &executable).is_err());
|
||||
let mut wrong_length = base;
|
||||
wrong_length.data.pop();
|
||||
let wrong_length = kb_rpc::AccountInfoResult {
|
||||
context: kb_rpc::RpcResponseContext {
|
||||
slot: 80,
|
||||
api_version: std::option::Option::None,
|
||||
},
|
||||
account: std::option::Option::Some(wrong_length),
|
||||
};
|
||||
assert!(crate::solana_elgamal_registry_stateful::materialize_elgamal_registry_account_info_result(&request, &wrong_length).is_err());
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,918 @@
|
||||
// file: kb_pipeline/src/solana_memo_execution.rs
|
||||
// version: 2
|
||||
|
||||
//! Devnet SPL Memo v4 execution with canonical post-validation.
|
||||
|
||||
/// Complete request for one SPL Memo v4 Devnet execution.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct DevnetMemoExecutionRequest {
|
||||
/// Stable caller-provided execution identifier.
|
||||
pub intent_id: std::string::String,
|
||||
/// Endpoint role used for cluster, balance, blockhash, fee and hydration calls.
|
||||
pub query_role: std::string::String,
|
||||
/// Endpoint role used for simulation, submission and confirmation polling.
|
||||
pub transaction_role: std::string::String,
|
||||
/// Exact UTF-8 Memo payload.
|
||||
pub message: std::string::String,
|
||||
/// Supplies the persistent Devnet fee payer as a Memo signer account.
|
||||
pub include_wallet_as_memo_signer: bool,
|
||||
/// Explicitly authorizes signing and submission after successful simulation.
|
||||
pub submit: bool,
|
||||
/// Explicit operator confirmation required by the active profile.
|
||||
pub operator_confirmed: bool,
|
||||
/// Number of `getTransaction` retries after the first hydration attempt.
|
||||
pub post_validation_max_retries: u32,
|
||||
/// Replaces existing core and decode outputs for the submitted signature.
|
||||
pub force_post_validation_replay: bool,
|
||||
}
|
||||
|
||||
impl DevnetMemoExecutionRequest {
|
||||
/// Creates a conservative simulation-only Memo v4 request.
|
||||
pub fn new(
|
||||
intent_id: impl std::convert::Into<std::string::String>,
|
||||
message: impl std::convert::Into<std::string::String>,
|
||||
) -> Self {
|
||||
return Self {
|
||||
intent_id: intent_id.into(),
|
||||
query_role: "http_queries".to_string(),
|
||||
transaction_role: "http_transactions".to_string(),
|
||||
message: message.into(),
|
||||
include_wallet_as_memo_signer: true,
|
||||
submit: false,
|
||||
operator_confirmed: false,
|
||||
post_validation_max_retries: 10,
|
||||
force_post_validation_replay: false,
|
||||
};
|
||||
}
|
||||
|
||||
/// Validates request-local bounds independently from one profile.
|
||||
pub fn validate(&self) -> kb_core::Result<()> {
|
||||
if self.intent_id.trim().is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Devnet Memo execution intent id must not be empty",
|
||||
));
|
||||
}
|
||||
if self.query_role.trim().is_empty() || self.transaction_role.trim().is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Devnet Memo execution endpoint roles must not be empty",
|
||||
));
|
||||
}
|
||||
if self.message.len() > kb_executor_spl_memo::MAX_MEMO_MESSAGE_BYTES {
|
||||
return std::result::Result::Err(kb_core::Error::config(format!(
|
||||
"Devnet Memo payload length {} exceeds the executor limit {}",
|
||||
self.message.len(),
|
||||
kb_executor_spl_memo::MAX_MEMO_MESSAGE_BYTES
|
||||
)));
|
||||
}
|
||||
if self.post_validation_max_retries > 20 {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"post-execution getTransaction retries must not exceed 20",
|
||||
));
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
/// Complete result of one SPL Memo v4 Devnet execution.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct DevnetMemoExecutionSummary {
|
||||
/// Profile used by the orchestration.
|
||||
pub profile_name: std::string::String,
|
||||
/// Exact classified cluster.
|
||||
pub cluster: kb_execution_api::ExecutionCluster,
|
||||
/// Genesis hash returned by the selected endpoint.
|
||||
pub genesis_hash: std::string::String,
|
||||
/// Non-secret persistent wallet description.
|
||||
pub wallet: kb_wallet::WalletSummary,
|
||||
/// Wallet balance observed before planning.
|
||||
pub balance_lamports: u64,
|
||||
/// Exact prepared Memo plan.
|
||||
pub plan: kb_execution_api::PreparedExecutionPlan,
|
||||
/// Recent blockhash used by the exact transaction.
|
||||
pub latest_blockhash: kb_rpc::LatestBlockhashResult,
|
||||
/// Fee estimate for the exact compiled message.
|
||||
pub fee: kb_rpc::FeeForMessageResult,
|
||||
/// Exact simulation result bound to the compiled message.
|
||||
pub simulation: kb_execution_api::ExecutionSimulationResult,
|
||||
/// Submission result when explicitly authorized.
|
||||
pub send_result: std::option::Option<kb_execution_api::ExecutionSendResult>,
|
||||
/// Confirmation result when submitted.
|
||||
pub confirmation: std::option::Option<kb_execution_api::ExecutionConfirmationResult>,
|
||||
/// Canonical hydration result for the exact signature.
|
||||
pub backfill: std::option::Option<crate::BackfillSummary>,
|
||||
/// Core extraction result for the exact signature.
|
||||
pub core_extraction: std::option::Option<crate::CoreExtractionSummary>,
|
||||
/// First Memo decode and materialization replay.
|
||||
pub decode_replay: std::option::Option<crate::DecodeReplaySummary>,
|
||||
/// Second replay proving that the same decoder version and input are idempotent.
|
||||
pub idempotence_replay: std::option::Option<crate::DecodeReplaySummary>,
|
||||
/// Exact persisted transaction annotation rows for the submitted signature.
|
||||
pub annotations: std::vec::Vec<kb_store_core::MaterializedEventQueryRow>,
|
||||
/// Aggregated post-execution validation diagnostic.
|
||||
pub post_execution: std::option::Option<kb_execution_api::PostExecutionDiagnostic>,
|
||||
}
|
||||
|
||||
/// Executes one Memo v4 Devnet simulation or explicitly authorized submission.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn execute_devnet_memo<S, O>(
|
||||
http_pool: &kb_rpc::HttpEndpointPool,
|
||||
store: &S,
|
||||
profile: &kb_config::ProfileConfig,
|
||||
workspace_root: &std::path::Path,
|
||||
request: &crate::DevnetMemoExecutionRequest,
|
||||
decoders: &[std::sync::Arc<dyn kb_decoder_api::InstructionDecoder>],
|
||||
materializers: &[std::sync::Arc<dyn kb_materializer_api::EventMaterializer>],
|
||||
observer: &O,
|
||||
) -> kb_core::Result<crate::DevnetMemoExecutionSummary>
|
||||
where
|
||||
S: kb_store_core::RawTransactionStore
|
||||
+ kb_store_core::CoreExtractionStore
|
||||
+ kb_store_core::DecodePipelineStore
|
||||
+ Sync,
|
||||
O: crate::SolanaExecutionObserver,
|
||||
{
|
||||
if let std::result::Result::Err(error) = request.validate() {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
if let std::result::Result::Err(error) =
|
||||
crate::solana_memo_execution::validate_profile(profile, request)
|
||||
{
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
if let std::result::Result::Err(error) =
|
||||
crate::solana_execution::ensure_not_cancelled(observer, "validate")
|
||||
{
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let genesis = match http_pool.get_genesis_hash_for_role(request.query_role.as_str()).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if genesis.classified_cluster
|
||||
!= std::option::Option::Some(kb_execution_api::ExecutionCluster::Devnet)
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_cluster_mismatch",
|
||||
format!(
|
||||
"expected Devnet genesis hash but endpoint returned {} classified as {:?}",
|
||||
genesis.genesis_hash, genesis.classified_cluster
|
||||
),
|
||||
));
|
||||
}
|
||||
let wallet = match crate::solana_execution::load_profile_wallet(profile, workspace_root).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let wallet_summary = wallet.summary();
|
||||
let fee_payer = kb_model::Pubkey(wallet_summary.public_key.clone());
|
||||
let balance = match http_pool
|
||||
.get_balance_for_role(
|
||||
request.query_role.as_str(),
|
||||
&fee_payer,
|
||||
&kb_rpc::GetBalanceConfig::confirmed(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if balance.lamports < profile.execution.max_fee_lamports {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_balance_insufficient",
|
||||
format!(
|
||||
"Devnet wallet balance {} is below the configured fee ceiling {}",
|
||||
balance.lamports, profile.execution.max_fee_lamports
|
||||
),
|
||||
));
|
||||
}
|
||||
let plan = match crate::solana_memo_execution::build_plan(profile, request, fee_payer.clone()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let plan_evaluation =
|
||||
match kb_execution_safety::ExecutionSafetyChecker.evaluate_prepared_plan(&plan) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if plan_evaluation.decision == kb_execution_safety::ExecutionSafetyDecision::Deny {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_plan_denied",
|
||||
crate::solana_execution::violation_message(plan_evaluation.violations.as_slice()),
|
||||
));
|
||||
}
|
||||
let latest_blockhash = match http_pool
|
||||
.get_latest_blockhash_for_role(
|
||||
request.query_role.as_str(),
|
||||
&kb_rpc::GetLatestBlockhashConfig::confirmed(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let unsigned = match kb_execution_solana::build_legacy_transaction(
|
||||
&plan,
|
||||
latest_blockhash.blockhash.as_str(),
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let message_base64 = unsigned.message_base64();
|
||||
let fee = match http_pool
|
||||
.get_fee_for_message_for_role(
|
||||
request.query_role.as_str(),
|
||||
message_base64.as_str(),
|
||||
&kb_rpc::GetFeeForMessageConfig::new(
|
||||
kb_rpc::RpcCommitmentLevel::Confirmed,
|
||||
std::option::Option::Some(latest_blockhash.context.slot),
|
||||
),
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if fee.fee_lamports.is_none() {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_fee_unavailable",
|
||||
"getFeeForMessage returned null for the selected recent blockhash",
|
||||
));
|
||||
}
|
||||
let unsigned_base64 = match unsigned.transaction_base64() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let simulation_config = match kb_rpc::SimulateTransactionConfig::new(
|
||||
kb_rpc::RpcCommitmentLevel::Confirmed,
|
||||
false,
|
||||
false,
|
||||
std::option::Option::Some(latest_blockhash.context.slot),
|
||||
true,
|
||||
std::option::Option::None,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
crate::solana_execution::emit(
|
||||
observer,
|
||||
crate::SolanaExecutionProgressLevel::Info,
|
||||
"memo_simulation",
|
||||
format!("simulating exact Memo message {}", unsigned.message_hash()),
|
||||
std::option::Option::None,
|
||||
);
|
||||
let simulation_rpc = match http_pool
|
||||
.simulate_transaction_for_role(
|
||||
request.transaction_role.as_str(),
|
||||
unsigned_base64.as_str(),
|
||||
&simulation_config,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let simulation = simulation_rpc.to_execution_result(
|
||||
kb_execution_api::ExecutionCluster::Devnet,
|
||||
kb_execution_api::ExecutionBlockhashKind::Latest,
|
||||
std::option::Option::Some(
|
||||
simulation_rpc.context.slot.saturating_sub(latest_blockhash.context.slot),
|
||||
),
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
std::option::Option::Some(&fee),
|
||||
);
|
||||
let evidence = unsigned.bind_simulation(simulation.clone());
|
||||
let mut summary = crate::DevnetMemoExecutionSummary {
|
||||
profile_name: profile.name.clone(),
|
||||
cluster: kb_execution_api::ExecutionCluster::Devnet,
|
||||
genesis_hash: genesis.genesis_hash,
|
||||
wallet: wallet_summary,
|
||||
balance_lamports: balance.lamports,
|
||||
plan,
|
||||
latest_blockhash,
|
||||
fee,
|
||||
simulation,
|
||||
send_result: std::option::Option::None,
|
||||
confirmation: std::option::Option::None,
|
||||
backfill: std::option::Option::None,
|
||||
core_extraction: std::option::Option::None,
|
||||
decode_replay: std::option::Option::None,
|
||||
idempotence_replay: std::option::Option::None,
|
||||
annotations: std::vec::Vec::new(),
|
||||
post_execution: std::option::Option::None,
|
||||
};
|
||||
if !request.submit {
|
||||
return std::result::Result::Ok(summary);
|
||||
}
|
||||
if !summary.simulation.success {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_simulation_failed",
|
||||
crate::solana_execution::simulation_failure_message(&summary.simulation),
|
||||
));
|
||||
}
|
||||
let send_evaluation = match kb_execution_safety::ExecutionSafetyChecker
|
||||
.evaluate_send(&summary.plan, &summary.simulation)
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if send_evaluation.decision == kb_execution_safety::ExecutionSafetyDecision::Deny {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_send_denied",
|
||||
crate::solana_execution::violation_message(send_evaluation.violations.as_slice()),
|
||||
));
|
||||
}
|
||||
let signed = match unsigned.sign_after_simulation(&evidence, &[wallet.as_signer()]) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if let std::result::Result::Err(error) = signed.verify_signatures() {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let signature = signed.primary_signature().clone();
|
||||
let mut diagnostic = kb_execution_api::PostExecutionDiagnostic {
|
||||
signature: signature.clone(),
|
||||
canonical_inserted: false,
|
||||
core_extracted: false,
|
||||
decode_replayed: false,
|
||||
materialized: false,
|
||||
diagnostics: std::vec::Vec::new(),
|
||||
};
|
||||
let send_config = match kb_rpc::SendTransactionConfig::from_execution_config(
|
||||
&profile.execution,
|
||||
std::option::Option::Some(summary.latest_blockhash.context.slot),
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let signed_base64 = signed.transaction_base64();
|
||||
let sent = match http_pool
|
||||
.send_transaction_for_role(
|
||||
request.transaction_role.as_str(),
|
||||
signed_base64.as_str(),
|
||||
&signature,
|
||||
&send_config,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
diagnostic.diagnostics.push(format!("Memo submission failed: {error}"));
|
||||
summary.post_execution = std::option::Option::Some(diagnostic);
|
||||
return std::result::Result::Ok(summary);
|
||||
},
|
||||
};
|
||||
summary.send_result = std::option::Option::Some(
|
||||
sent.to_execution_result(kb_execution_api::ExecutionCluster::Devnet),
|
||||
);
|
||||
let confirmation_config = match kb_rpc::ConfirmTransactionConfig::from_execution_config(
|
||||
&profile.execution,
|
||||
std::option::Option::Some(summary.latest_blockhash.last_valid_block_height),
|
||||
std::option::Option::Some(summary.latest_blockhash.context.slot),
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let confirmation = match http_pool
|
||||
.confirm_transaction_for_roles(
|
||||
request.transaction_role.as_str(),
|
||||
request.query_role.as_str(),
|
||||
kb_execution_api::ExecutionCluster::Devnet,
|
||||
&signature,
|
||||
&confirmation_config,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
diagnostic.diagnostics.push(format!("Memo confirmation failed: {error}"));
|
||||
summary.post_execution = std::option::Option::Some(diagnostic);
|
||||
return std::result::Result::Ok(summary);
|
||||
},
|
||||
};
|
||||
let confirmation_status = confirmation.status;
|
||||
summary.confirmation = std::option::Option::Some(confirmation);
|
||||
if !matches!(
|
||||
confirmation_status,
|
||||
kb_execution_api::ExecutionConfirmationStatus::Confirmed
|
||||
| kb_execution_api::ExecutionConfirmationStatus::Finalized
|
||||
) {
|
||||
diagnostic.diagnostics.push(format!(
|
||||
"Memo post-validation stopped at confirmation status {confirmation_status:?}"
|
||||
));
|
||||
summary.post_execution = std::option::Option::Some(diagnostic);
|
||||
return std::result::Result::Ok(summary);
|
||||
}
|
||||
let backfill = match crate::solana_memo_execution::hydrate_signature(
|
||||
http_pool, store, profile, request, observer, &signature,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
diagnostic.diagnostics.push(format!("Memo hydration failed: {error}"));
|
||||
summary.post_execution = std::option::Option::Some(diagnostic);
|
||||
return std::result::Result::Ok(summary);
|
||||
},
|
||||
};
|
||||
diagnostic.canonical_inserted = crate::solana_memo_execution::canonical_available(&backfill);
|
||||
summary.backfill = std::option::Option::Some(backfill);
|
||||
if !diagnostic.canonical_inserted {
|
||||
diagnostic
|
||||
.diagnostics
|
||||
.push("confirmed Memo transaction was unavailable for canonical hydration".to_string());
|
||||
summary.post_execution = std::option::Option::Some(diagnostic);
|
||||
return std::result::Result::Ok(summary);
|
||||
}
|
||||
let extraction = match crate::execute_core_extraction(
|
||||
store,
|
||||
&crate::CoreExtractionRequest {
|
||||
source: crate::CoreExtractionSource::Signatures(std::vec![signature.0.clone()]),
|
||||
limit: 1,
|
||||
max_concurrent_extractions: 1,
|
||||
force_replay: request.force_post_validation_replay,
|
||||
},
|
||||
observer,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
diagnostic.diagnostics.push(format!("Memo core extraction failed: {error}"));
|
||||
summary.post_execution = std::option::Option::Some(diagnostic);
|
||||
return std::result::Result::Ok(summary);
|
||||
},
|
||||
};
|
||||
diagnostic.core_extracted = extraction.failed == 0
|
||||
&& !extraction.cancelled
|
||||
&& extraction.selected == 1
|
||||
&& extraction.extracted.saturating_add(extraction.skipped) >= 1;
|
||||
summary.core_extraction = std::option::Option::Some(extraction);
|
||||
if !diagnostic.core_extracted {
|
||||
diagnostic.diagnostics.push("Memo core extraction did not complete".to_string());
|
||||
summary.post_execution = std::option::Option::Some(diagnostic);
|
||||
return std::result::Result::Ok(summary);
|
||||
}
|
||||
let first_replay = match crate::solana_memo_execution::replay_memo(
|
||||
store,
|
||||
request,
|
||||
&signature,
|
||||
false,
|
||||
decoders,
|
||||
materializers,
|
||||
observer,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
diagnostic.diagnostics.push(format!("Memo decode replay failed: {error}"));
|
||||
summary.post_execution = std::option::Option::Some(diagnostic);
|
||||
return std::result::Result::Ok(summary);
|
||||
},
|
||||
};
|
||||
diagnostic.decode_replayed = crate::solana_memo_execution::decode_completed(&first_replay);
|
||||
summary.decode_replay = std::option::Option::Some(first_replay);
|
||||
let filter = match kb_store_core::MaterializedEventFilter::new(
|
||||
std::option::Option::Some("transaction_annotations".to_string()),
|
||||
std::option::Option::Some("transaction_annotation".to_string()),
|
||||
std::option::Option::Some(signature.0.clone()),
|
||||
8,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
summary.annotations =
|
||||
match kb_store_core::DecodePipelineStore::list_materialized_events(store, &filter).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
diagnostic.diagnostics.push(format!("Memo annotation query failed: {error}"));
|
||||
summary.post_execution = std::option::Option::Some(diagnostic);
|
||||
return std::result::Result::Ok(summary);
|
||||
},
|
||||
};
|
||||
diagnostic.materialized = diagnostic.decode_replayed
|
||||
&& summary
|
||||
.annotations
|
||||
.iter()
|
||||
.any(|row| return row.signature.as_str() == signature.0.as_str());
|
||||
let second_replay = match crate::solana_memo_execution::replay_memo(
|
||||
store,
|
||||
request,
|
||||
&signature,
|
||||
true,
|
||||
decoders,
|
||||
materializers,
|
||||
observer,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
diagnostic.diagnostics.push(format!("Memo idempotence replay failed: {error}"));
|
||||
summary.post_execution = std::option::Option::Some(diagnostic);
|
||||
return std::result::Result::Ok(summary);
|
||||
},
|
||||
};
|
||||
let idempotent = second_replay.failed_inputs == 0
|
||||
&& second_replay.processors.iter().all(|processor| {
|
||||
return processor.failed == 0
|
||||
&& processor.materialized_outputs == 0
|
||||
&& processor.materialization_refused == 0;
|
||||
});
|
||||
summary.idempotence_replay = std::option::Option::Some(second_replay);
|
||||
if !idempotent {
|
||||
diagnostic
|
||||
.diagnostics
|
||||
.push("second Memo replay did not prove a clean idempotent skip".to_string());
|
||||
} else if diagnostic.canonical_inserted
|
||||
&& diagnostic.core_extracted
|
||||
&& diagnostic.decode_replayed
|
||||
&& diagnostic.materialized
|
||||
{
|
||||
diagnostic.diagnostics.push(
|
||||
"Memo completed canonical hydration, core extraction, decode, annotation projection and idempotence validation"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
summary.post_execution = std::option::Option::Some(diagnostic);
|
||||
return std::result::Result::Ok(summary);
|
||||
}
|
||||
|
||||
fn validate_profile(
|
||||
profile: &kb_config::ProfileConfig,
|
||||
request: &crate::DevnetMemoExecutionRequest,
|
||||
) -> kb_core::Result<()> {
|
||||
if profile.wallet.cluster != "devnet" {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Memo Devnet orchestration requires a Devnet wallet profile",
|
||||
));
|
||||
}
|
||||
if !profile.wallet.temporary_wallet_enabled || !profile.wallet.temporary_wallet_persist {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Memo Devnet orchestration requires an enabled persistent temporary wallet",
|
||||
));
|
||||
}
|
||||
if !profile.execution.require_simulation {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Memo Devnet orchestration requires simulation",
|
||||
));
|
||||
}
|
||||
if request.submit && !profile.wallet.devnet_send_enabled {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Devnet transaction submission is disabled by the wallet profile",
|
||||
));
|
||||
}
|
||||
if request.submit
|
||||
&& profile.execution.require_operator_confirmation
|
||||
&& !request.operator_confirmed
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Memo Devnet submission requires explicit operator confirmation",
|
||||
));
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn build_plan(
|
||||
profile: &kb_config::ProfileConfig,
|
||||
request: &crate::DevnetMemoExecutionRequest,
|
||||
fee_payer: kb_model::Pubkey,
|
||||
) -> kb_core::Result<kb_execution_api::PreparedExecutionPlan> {
|
||||
let signers = if request.include_wallet_as_memo_signer {
|
||||
std::vec![kb_executor_spl_memo::SplMemoSigner { pubkey: fee_payer.clone() }]
|
||||
} else {
|
||||
std::vec::Vec::new()
|
||||
};
|
||||
let intent = kb_executor_spl_memo::SplMemoExecutionIntent {
|
||||
intent_id: request.intent_id.clone(),
|
||||
fee_payer: fee_payer.clone(),
|
||||
policy: kb_execution_api::ExecutionPolicy {
|
||||
cluster: kb_execution_api::ExecutionClusterPolicy {
|
||||
expected_cluster: kb_execution_api::ExecutionCluster::Devnet,
|
||||
allow_mainnet: false,
|
||||
mainnet_confirmation: false,
|
||||
},
|
||||
simulation: kb_execution_api::ExecutionSimulationPolicy::Required,
|
||||
blockhash: kb_execution_api::ExecutionBlockhashPolicy {
|
||||
kind: kb_execution_api::ExecutionBlockhashKind::Latest,
|
||||
max_age_slots: std::option::Option::Some(
|
||||
profile.execution.recent_blockhash_max_age_slots,
|
||||
),
|
||||
nonce_account: std::option::Option::None,
|
||||
nonce_authority: std::option::Option::None,
|
||||
},
|
||||
cost_limit: kb_execution_api::ExecutionCostLimit {
|
||||
max_spend_lamports: std::option::Option::Some(0),
|
||||
max_fee_lamports: std::option::Option::Some(profile.execution.max_fee_lamports),
|
||||
max_compute_unit_price_micro_lamports: std::option::Option::Some(
|
||||
profile.execution.max_compute_unit_price_micro_lamports,
|
||||
),
|
||||
},
|
||||
authorized_signers: std::vec![fee_payer.clone()],
|
||||
dry_run: !request.submit,
|
||||
post_execution_validation: kb_execution_api::PostExecutionValidationPolicy {
|
||||
canonical_insert_required: true,
|
||||
core_extraction_required: true,
|
||||
decode_replay_required: true,
|
||||
materialization_required: true,
|
||||
},
|
||||
},
|
||||
operation: kb_executor_spl_memo::SplMemoOperation::AddMemo {
|
||||
generation: kb_executor_spl_memo::SplMemoGeneration::V4,
|
||||
message: request.message.clone(),
|
||||
signers,
|
||||
},
|
||||
};
|
||||
return kb_execution_api::TypedInstructionExecutor::build_prepared_plan(
|
||||
&kb_executor_spl_memo::SplMemoExecutor,
|
||||
&intent,
|
||||
);
|
||||
}
|
||||
|
||||
async fn hydrate_signature<S, O>(
|
||||
http_pool: &kb_rpc::HttpEndpointPool,
|
||||
store: &S,
|
||||
profile: &kb_config::ProfileConfig,
|
||||
request: &crate::DevnetMemoExecutionRequest,
|
||||
observer: &O,
|
||||
signature: &kb_model::Signature,
|
||||
) -> kb_core::Result<crate::BackfillSummary>
|
||||
where
|
||||
S: kb_store_core::RawTransactionStore + Sync,
|
||||
O: crate::SolanaExecutionObserver,
|
||||
{
|
||||
let mut retry = 0_u32;
|
||||
loop {
|
||||
let result = match crate::execute_http_backfill(
|
||||
http_pool,
|
||||
store,
|
||||
&crate::BackfillRequest {
|
||||
role: request.query_role.clone(),
|
||||
commitment: "confirmed".to_string(),
|
||||
source: crate::BackfillSource::ExplicitSignatures(std::vec![signature.0.clone()]),
|
||||
page_size: 1,
|
||||
max_pages: 1,
|
||||
max_concurrent_requests: 1,
|
||||
max_retries: 0,
|
||||
},
|
||||
observer,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if crate::solana_memo_execution::canonical_available(&result)
|
||||
|| retry >= request.post_validation_max_retries
|
||||
|| observer.is_execution_cancelled()
|
||||
{
|
||||
return std::result::Result::Ok(result);
|
||||
}
|
||||
retry = retry.saturating_add(1);
|
||||
tokio::time::sleep(std::time::Duration::from_millis(std::cmp::max(
|
||||
profile.execution.confirmation_poll_interval_ms,
|
||||
500,
|
||||
)))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
fn canonical_available(summary: &crate::BackfillSummary) -> bool {
|
||||
return summary.failed == 0
|
||||
&& summary.missing == 0
|
||||
&& summary.candidates_completed == 1
|
||||
&& summary.candidates_cancelled == 0
|
||||
&& summary.candidates_not_started == 0
|
||||
&& summary
|
||||
.canonical_inserted
|
||||
.saturating_add(summary.canonical_skipped)
|
||||
.saturating_add(summary.existing_skipped)
|
||||
>= 1;
|
||||
}
|
||||
|
||||
async fn replay_memo<S, O>(
|
||||
store: &S,
|
||||
request: &crate::DevnetMemoExecutionRequest,
|
||||
signature: &kb_model::Signature,
|
||||
include_materialized_state: bool,
|
||||
decoders: &[std::sync::Arc<dyn kb_decoder_api::InstructionDecoder>],
|
||||
materializers: &[std::sync::Arc<dyn kb_materializer_api::EventMaterializer>],
|
||||
observer: &O,
|
||||
) -> kb_core::Result<crate::DecodeReplaySummary>
|
||||
where
|
||||
S: kb_store_core::DecodePipelineStore + Sync,
|
||||
O: crate::SolanaExecutionObserver,
|
||||
{
|
||||
let mut states = std::vec![
|
||||
kb_store_core::CoreInstructionProcessingState::Pending,
|
||||
kb_store_core::CoreInstructionProcessingState::Failed,
|
||||
kb_store_core::CoreInstructionProcessingState::ReplayRequested,
|
||||
];
|
||||
if include_materialized_state {
|
||||
states.push(kb_store_core::CoreInstructionProcessingState::Materialized);
|
||||
}
|
||||
let selection = match kb_store_core::DecodeSelectionFilter::new(
|
||||
std::vec![signature.0.clone()],
|
||||
states,
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
std::vec![kb_program_ids::SPL_MEMO_V4_PROGRAM_ID.to_string()],
|
||||
std::vec::Vec::new(),
|
||||
false,
|
||||
8,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return crate::execute_decode_replay(
|
||||
store,
|
||||
&crate::DecodeReplayRequest {
|
||||
campaign_id: crate::new_decode_campaign_id(),
|
||||
selection,
|
||||
decoder_names: std::vec::Vec::new(),
|
||||
dispatch_policy: crate::DecodeDispatchPolicy::HighestPriority,
|
||||
max_concurrent_inputs: 1,
|
||||
force_replay: if include_materialized_state {
|
||||
false
|
||||
} else {
|
||||
request.force_post_validation_replay
|
||||
},
|
||||
force_replay_all_matching: false,
|
||||
materialize_after_decode: true,
|
||||
},
|
||||
decoders,
|
||||
materializers,
|
||||
observer,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
fn decode_completed(summary: &crate::DecodeReplaySummary) -> bool {
|
||||
return summary.failed_inputs == 0
|
||||
&& summary.unmatched == 0
|
||||
&& !summary.cancelled
|
||||
&& summary.completed >= 1
|
||||
&& summary.processors.iter().all(|processor| {
|
||||
return processor.failed == 0
|
||||
&& processor.unsupported == 0
|
||||
&& processor.materialization_refused == 0;
|
||||
})
|
||||
&& summary.processors.iter().map(|processor| return processor.decoded).sum::<u64>() >= 1;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
fn local_devnet_profile() -> kb_config::ProfileConfig {
|
||||
let config =
|
||||
match kb_config::parse_config_json(include_str!("../../config/example.config.json")) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("example config parse failed: {error}"),
|
||||
};
|
||||
for profile in config.profiles {
|
||||
if profile.name == "local_devnet" {
|
||||
return profile;
|
||||
}
|
||||
}
|
||||
panic!("local_devnet profile missing");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn request_is_simulation_only_and_bounded_by_default() {
|
||||
let request = crate::DevnetMemoExecutionRequest::new("memo-1", "audit annotation");
|
||||
assert!(!request.submit);
|
||||
assert!(request.include_wallet_as_memo_signer);
|
||||
assert!(request.validate().is_ok());
|
||||
let oversized = "x".repeat(kb_executor_spl_memo::MAX_MEMO_MESSAGE_BYTES + 1);
|
||||
assert!(crate::DevnetMemoExecutionRequest::new("memo-2", oversized).validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_v4_plan_has_zero_spend_and_wallet_signer() {
|
||||
let profile = crate::solana_memo_execution::tests::local_devnet_profile();
|
||||
let fee_payer = kb_model::Pubkey(kb_program_ids::SYSTEM_PROGRAM_ID.to_string());
|
||||
let mut request = crate::DevnetMemoExecutionRequest::new("memo-3", "hello");
|
||||
request.submit = true;
|
||||
request.operator_confirmed = true;
|
||||
let plan =
|
||||
match crate::solana_memo_execution::build_plan(&profile, &request, fee_payer.clone()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("Memo plan failed: {error}"),
|
||||
};
|
||||
assert_eq!(plan.requested_spend_lamports, 0);
|
||||
assert_eq!(plan.fee_payer, fee_payer);
|
||||
assert_eq!(plan.instructions.len(), 1);
|
||||
assert_eq!(plan.instructions[0].program_id.0, kb_program_ids::SPL_MEMO_V4_PROGRAM_ID);
|
||||
assert_eq!(plan.instructions[0].accounts.len(), 1);
|
||||
assert!(plan.instructions[0].accounts[0].is_signer);
|
||||
assert!(!plan.instructions[0].accounts[0].is_writable);
|
||||
assert!(
|
||||
kb_execution_safety::ExecutionSafetyChecker
|
||||
.evaluate_prepared_plan(&plan)
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn submission_requires_profile_enablement_and_confirmation() {
|
||||
let profile = crate::solana_memo_execution::tests::local_devnet_profile();
|
||||
let mut request = crate::DevnetMemoExecutionRequest::new("memo-4", "hello");
|
||||
assert!(crate::solana_memo_execution::validate_profile(&profile, &request).is_ok());
|
||||
request.submit = true;
|
||||
assert!(crate::solana_memo_execution::validate_profile(&profile, &request).is_err());
|
||||
request.operator_confirmed = true;
|
||||
assert!(crate::solana_memo_execution::validate_profile(&profile, &request).is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn optional_devnet_memo_execution_from_env() {
|
||||
if std::env::var("KB_DEVNET_MEMO_EXECUTION_TEST").ok().as_deref()
|
||||
!= std::option::Option::Some("1")
|
||||
{
|
||||
return;
|
||||
}
|
||||
let database_url = match std::env::var("KB_POSTGRES_TEST_URL") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
panic!("KB_POSTGRES_TEST_URL is required: {error}");
|
||||
},
|
||||
};
|
||||
let mut profile = crate::solana_memo_execution::tests::local_devnet_profile();
|
||||
profile.database.backend = "postgres".to_string();
|
||||
profile.database.postgres.url = database_url;
|
||||
if let std::result::Result::Ok(directory) = std::env::var("KB_DEVNET_WALLET_DIR") {
|
||||
profile.wallet.wallet_dir = directory;
|
||||
}
|
||||
let pool = match kb_rpc::HttpEndpointPool::from_profile(&profile) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("HTTP pool creation failed: {error}"),
|
||||
};
|
||||
let store = match kb_store_pg::PostgresStore::connect_from_profile_config(&profile).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("PostgreSQL connection failed: {error}"),
|
||||
};
|
||||
if let std::result::Result::Err(error) = store.initialize_store_schema().await {
|
||||
panic!("PostgreSQL schema initialization failed: {error}");
|
||||
}
|
||||
let mut request = crate::DevnetMemoExecutionRequest::new(
|
||||
format!("devnet-memo-test-{}", uuid::Uuid::new_v4()),
|
||||
format!("khadhroony-bot2 memo validation {}", uuid::Uuid::new_v4()),
|
||||
);
|
||||
request.post_validation_max_retries = 20;
|
||||
if std::env::var("KB_DEVNET_MEMO_SUBMIT").ok().as_deref() == std::option::Option::Some("1")
|
||||
{
|
||||
request.submit = true;
|
||||
request.operator_confirmed = true;
|
||||
}
|
||||
let decoders: std::vec::Vec<std::sync::Arc<dyn kb_decoder_api::InstructionDecoder>> =
|
||||
std::vec![std::sync::Arc::new(kb_decoder_spl_memo::SplMemoDecoder)];
|
||||
let materializers: std::vec::Vec<
|
||||
std::sync::Arc<dyn kb_materializer_api::EventMaterializer>,
|
||||
> = std::vec![std::sync::Arc::new(
|
||||
kb_materializer_transaction_annotations::TransactionAnnotationMaterializer,
|
||||
)];
|
||||
let workspace_root = match std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => panic!("workspace root cannot be resolved"),
|
||||
};
|
||||
let summary = match crate::execute_devnet_memo(
|
||||
&pool,
|
||||
&store,
|
||||
&profile,
|
||||
workspace_root,
|
||||
&request,
|
||||
decoders.as_slice(),
|
||||
materializers.as_slice(),
|
||||
&crate::NoopSolanaExecutionObserver,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("Devnet Memo execution failed: {error}"),
|
||||
};
|
||||
assert!(summary.simulation.success);
|
||||
if request.submit {
|
||||
let post_execution = match summary.post_execution {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => panic!("Memo post-execution diagnostic missing"),
|
||||
};
|
||||
assert!(post_execution.canonical_inserted);
|
||||
assert!(post_execution.core_extracted);
|
||||
assert!(post_execution.decode_replayed);
|
||||
assert!(post_execution.materialized);
|
||||
assert!(!summary.annotations.is_empty());
|
||||
let idempotence = match summary.idempotence_replay {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => panic!("Memo idempotence replay missing"),
|
||||
};
|
||||
assert_eq!(
|
||||
idempotence
|
||||
.processors
|
||||
.iter()
|
||||
.map(|processor| return processor.materialized_outputs)
|
||||
.sum::<u64>(),
|
||||
0
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,242 @@
|
||||
// file: kb_pipeline/src/solana_token_2022_correlation.rs
|
||||
// version: 1
|
||||
|
||||
//! Deterministic Token-2022 instruction-to-state correlation.
|
||||
|
||||
/// Correlation outcome between one committed instruction fact and one final state snapshot.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub enum Token2022CorrelationStatus {
|
||||
/// The final snapshot contains the extension expected by the instruction fact.
|
||||
Confirmed,
|
||||
/// The final snapshot does not contain the extension expected by the instruction fact.
|
||||
Contradicted,
|
||||
/// The instruction output is outside the supported correlation inventory.
|
||||
NotApplicable,
|
||||
}
|
||||
|
||||
/// Deterministic correlation report for one instruction output and one final snapshot.
|
||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct Token2022StateCorrelationReport {
|
||||
/// Stable caller-provided correlation identity.
|
||||
pub correlation_key: std::string::String,
|
||||
/// Canonical account identity checked against the snapshot.
|
||||
pub account: kb_model::Pubkey,
|
||||
/// Instruction operation or risk fact used for correlation.
|
||||
pub fact_code: std::string::String,
|
||||
/// Extension expected from the fact when the fact is supported.
|
||||
pub expected_extension: std::option::Option<std::string::String>,
|
||||
/// Final correlation outcome.
|
||||
pub status: Token2022CorrelationStatus,
|
||||
/// Snapshot context slot.
|
||||
pub snapshot_slot: u64,
|
||||
/// Whether the snapshot contains the expected extension.
|
||||
pub extension_present: std::option::Option<bool>,
|
||||
/// Explicit semantic limitation of this correlation.
|
||||
pub fact_only: bool,
|
||||
}
|
||||
|
||||
/// Correlates one materialized instruction fact with one authoritative Token-2022 snapshot.
|
||||
pub fn correlate_token_2022_instruction_with_snapshot(
|
||||
correlation_key: &str,
|
||||
account: &kb_model::Pubkey,
|
||||
instruction_output: &kb_materializer_api::MaterializedOutput,
|
||||
snapshot: &crate::Token2022StatefulSnapshotBundle,
|
||||
) -> kb_core::Result<Token2022StateCorrelationReport> {
|
||||
if correlation_key.trim().is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Token-2022 correlation key must not be empty",
|
||||
));
|
||||
}
|
||||
if snapshot.account_key != account.0 {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_correlation_account_mismatch",
|
||||
format!(
|
||||
"Token-2022 correlation expected account {}, got snapshot {}",
|
||||
account.0, snapshot.account_key
|
||||
),
|
||||
));
|
||||
}
|
||||
let fact_code = match instruction_output.payload_json.get("riskKind") {
|
||||
std::option::Option::Some(value) => value.as_str().unwrap_or("").to_string(),
|
||||
std::option::Option::None => instruction_output
|
||||
.payload_json
|
||||
.get("operation")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or("")
|
||||
.to_string(),
|
||||
};
|
||||
if fact_code.is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_correlation_fact_missing",
|
||||
"Token-2022 correlation requires riskKind or operation in the instruction output",
|
||||
));
|
||||
}
|
||||
let expected_extension = expected_extension(fact_code.as_str());
|
||||
let extension_present = expected_extension.map(|extension| {
|
||||
return snapshot.extension_names.iter().any(|candidate| return candidate == extension);
|
||||
});
|
||||
let status = match extension_present {
|
||||
std::option::Option::Some(true) => Token2022CorrelationStatus::Confirmed,
|
||||
std::option::Option::Some(false) => Token2022CorrelationStatus::Contradicted,
|
||||
std::option::Option::None => Token2022CorrelationStatus::NotApplicable,
|
||||
};
|
||||
return std::result::Result::Ok(Token2022StateCorrelationReport {
|
||||
correlation_key: correlation_key.to_string(),
|
||||
account: account.clone(),
|
||||
fact_code,
|
||||
expected_extension: expected_extension.map(str::to_string),
|
||||
status,
|
||||
snapshot_slot: snapshot.slot,
|
||||
extension_present,
|
||||
fact_only: true,
|
||||
});
|
||||
}
|
||||
|
||||
fn expected_extension(fact_code: &str) -> std::option::Option<&'static str> {
|
||||
return match fact_code {
|
||||
"default_account_state_configured"
|
||||
| "initialize_default_account_state"
|
||||
| "update_default_account_state" => std::option::Option::Some("default_account_state"),
|
||||
"required_transfer_memos_enabled"
|
||||
| "required_transfer_memos_disabled"
|
||||
| "enable_required_transfer_memos"
|
||||
| "disable_required_transfer_memos" => std::option::Option::Some("memo_transfer"),
|
||||
"cpi_guard_enabled" | "cpi_guard_disabled" | "enable_cpi_guard" | "disable_cpi_guard" => {
|
||||
std::option::Option::Some("cpi_guard")
|
||||
},
|
||||
"non_transferable_mint_configured" | "initialize_non_transferable_mint" => {
|
||||
std::option::Option::Some("non_transferable")
|
||||
},
|
||||
"pausable_mint_configured"
|
||||
| "mint_activity_paused"
|
||||
| "mint_activity_resumed"
|
||||
| "initialize_pausable_config"
|
||||
| "pause"
|
||||
| "resume" => std::option::Option::Some("pausable"),
|
||||
"permissioned_burn_configured" | "initialize_permissioned_burn" => {
|
||||
std::option::Option::Some("permissioned_burn")
|
||||
},
|
||||
"confidential_credits_enabled"
|
||||
| "confidential_credits_disabled"
|
||||
| "non_confidential_credits_enabled"
|
||||
| "non_confidential_credits_disabled"
|
||||
| "enable_confidential_credits"
|
||||
| "disable_confidential_credits"
|
||||
| "enable_non_confidential_credits"
|
||||
| "disable_non_confidential_credits" => {
|
||||
std::option::Option::Some("confidential_transfer_account")
|
||||
},
|
||||
"initialize_transfer_fee_config"
|
||||
| "set_transfer_fee"
|
||||
| "withdraw_withheld_tokens_from_mint"
|
||||
| "harvest_withheld_tokens_to_mint" => std::option::Option::Some("transfer_fee_config"),
|
||||
"withdraw_withheld_tokens_from_accounts" | "transfer_checked_with_fee" => {
|
||||
std::option::Option::Some("transfer_fee_amount")
|
||||
},
|
||||
"initialize_confidential_transfer_fee_config"
|
||||
| "withdraw_confidential_withheld_tokens_from_mint"
|
||||
| "harvest_confidential_withheld_tokens_to_mint"
|
||||
| "enable_confidential_harvest_to_mint"
|
||||
| "disable_confidential_harvest_to_mint" => {
|
||||
std::option::Option::Some("confidential_transfer_fee_config")
|
||||
},
|
||||
"withdraw_confidential_withheld_tokens_from_accounts"
|
||||
| "transfer_confidential_tokens_with_fee" => {
|
||||
std::option::Option::Some("confidential_transfer_fee_amount")
|
||||
},
|
||||
_ => std::option::Option::None,
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
fn snapshot(extension_names: &[&str]) -> crate::Token2022StatefulSnapshotBundle {
|
||||
return crate::Token2022StatefulSnapshotBundle {
|
||||
account_key: solana_pubkey::Pubkey::new_from_array([7u8; 32]).to_string(),
|
||||
slot: 42,
|
||||
state_kind: "mint".to_string(),
|
||||
extension_names: extension_names
|
||||
.iter()
|
||||
.map(|value| return (*value).to_string())
|
||||
.collect(),
|
||||
outputs: std::vec::Vec::new(),
|
||||
};
|
||||
}
|
||||
|
||||
fn output(field: &str, value: &str) -> kb_materializer_api::MaterializedOutput {
|
||||
let mut payload = serde_json::Map::new();
|
||||
payload.insert(field.to_string(), serde_json::Value::String(value.to_string()));
|
||||
return kb_materializer_api::MaterializedOutput {
|
||||
output_key: "fact:0".to_string(),
|
||||
family: kb_model::MaterializedEventFamily::Risk,
|
||||
payload_json: serde_json::Value::Object(payload),
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_fact_is_confirmed_or_contradicted_by_the_final_extension_inventory() {
|
||||
let snapshot = snapshot(&["pausable"]);
|
||||
let account = kb_model::Pubkey(snapshot.account_key.clone());
|
||||
let confirmed = super::correlate_token_2022_instruction_with_snapshot(
|
||||
"corr:pause",
|
||||
&account,
|
||||
&output("riskKind", "mint_activity_paused"),
|
||||
&snapshot,
|
||||
);
|
||||
assert!(confirmed.is_ok());
|
||||
if let std::result::Result::Ok(confirmed) = confirmed {
|
||||
assert_eq!(confirmed.status, super::Token2022CorrelationStatus::Confirmed);
|
||||
assert_eq!(confirmed.extension_present, std::option::Option::Some(true));
|
||||
}
|
||||
let contradicted = super::correlate_token_2022_instruction_with_snapshot(
|
||||
"corr:fee",
|
||||
&account,
|
||||
&output("operation", "initialize_transfer_fee_config"),
|
||||
&snapshot,
|
||||
);
|
||||
assert!(contradicted.is_ok());
|
||||
if let std::result::Result::Ok(contradicted) = contradicted {
|
||||
assert_eq!(contradicted.status, super::Token2022CorrelationStatus::Contradicted);
|
||||
assert_eq!(contradicted.extension_present, std::option::Option::Some(false));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_fact_is_explicitly_not_applicable_without_inventing_a_conclusion() {
|
||||
let snapshot = snapshot(&[]);
|
||||
let account = kb_model::Pubkey(snapshot.account_key.clone());
|
||||
let report = super::correlate_token_2022_instruction_with_snapshot(
|
||||
"corr:unknown",
|
||||
&account,
|
||||
&output("operation", "unknown_future_operation"),
|
||||
&snapshot,
|
||||
);
|
||||
assert!(report.is_ok());
|
||||
if let std::result::Result::Ok(report) = report {
|
||||
assert_eq!(report.status, super::Token2022CorrelationStatus::NotApplicable);
|
||||
assert_eq!(report.extension_present, std::option::Option::None);
|
||||
assert!(report.fact_only);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn correlation_identity_and_account_mismatch_fail_closed() {
|
||||
let snapshot = snapshot(&["memo_transfer"]);
|
||||
let account = kb_model::Pubkey(snapshot.account_key.clone());
|
||||
let empty = super::correlate_token_2022_instruction_with_snapshot(
|
||||
"",
|
||||
&account,
|
||||
&output("riskKind", "required_transfer_memos_enabled"),
|
||||
&snapshot,
|
||||
);
|
||||
assert!(empty.is_err());
|
||||
let wrong = kb_model::Pubkey(solana_pubkey::Pubkey::new_from_array([8u8; 32]).to_string());
|
||||
let mismatch = super::correlate_token_2022_instruction_with_snapshot(
|
||||
"corr:mismatch",
|
||||
&wrong,
|
||||
&output("riskKind", "required_transfer_memos_enabled"),
|
||||
&snapshot,
|
||||
);
|
||||
assert!(mismatch.is_err());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
// file: kb_pipeline/src/solana_token_2022_crypto_preflight.rs
|
||||
// version: 2
|
||||
|
||||
//! Bounded cryptographic preflight for Token-2022 proof context-state accounts.
|
||||
|
||||
/// Maximum number of distinct proof context-state accounts accepted by one request.
|
||||
pub const MAX_TOKEN_2022_PROOF_CONTEXTS: usize = 8;
|
||||
/// Generic metadata bytes retained by every ZK ElGamal proof context-state account.
|
||||
pub const ZK_PROOF_CONTEXT_META_BYTES: usize = 33;
|
||||
|
||||
/// One exact pre-verified proof context-state requirement.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Token2022ProofContextRequirement {
|
||||
/// Stable semantic role used in diagnostics.
|
||||
pub role: std::string::String,
|
||||
/// Canonical proof context-state account.
|
||||
pub account: kb_model::Pubkey,
|
||||
/// Exact proof kind expected by the Token-2022 builder.
|
||||
pub proof_type: kb_executor_solana_core::ZkElGamalProofType,
|
||||
/// Optional authority retained by the context-state account.
|
||||
pub expected_authority: std::option::Option<kb_model::Pubkey>,
|
||||
}
|
||||
|
||||
/// One bounded cryptographic preflight request.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Token2022CryptographicPreflightRequest {
|
||||
/// Endpoint role used by every account read.
|
||||
pub query_role: std::string::String,
|
||||
/// Optional minimum RPC context slot.
|
||||
pub min_context_slot: std::option::Option<u64>,
|
||||
/// Ordered proof context-state requirements.
|
||||
pub proof_contexts: std::vec::Vec<Token2022ProofContextRequirement>,
|
||||
}
|
||||
|
||||
/// One validated proof context-state result.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct Token2022ProofContextReport {
|
||||
/// Stable semantic role.
|
||||
pub role: std::string::String,
|
||||
/// Canonical proof context-state account.
|
||||
pub account: kb_model::Pubkey,
|
||||
/// Official one-byte proof discriminator.
|
||||
pub proof_discriminator: u8,
|
||||
/// Exact context-state size for the proof type.
|
||||
pub context_state_bytes: usize,
|
||||
/// Context slot returned by the endpoint.
|
||||
pub context_slot: u64,
|
||||
/// Ordered successful checks.
|
||||
pub checks: std::vec::Vec<std::string::String>,
|
||||
}
|
||||
|
||||
/// Complete cryptographic preflight report.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct Token2022CryptographicPreflightReport {
|
||||
/// Commitment used for all reads.
|
||||
pub commitment: std::string::String,
|
||||
/// Highest context slot observed across all reads.
|
||||
pub context_slot: u64,
|
||||
/// Ordered validated proof contexts.
|
||||
pub proof_contexts: std::vec::Vec<Token2022ProofContextReport>,
|
||||
}
|
||||
|
||||
/// Reads and validates all pre-verified proof context-state accounts required by one operation.
|
||||
pub async fn inspect_token_2022_cryptographic_preflight(
|
||||
pool: &kb_rpc::HttpEndpointPool,
|
||||
request: &crate::Token2022CryptographicPreflightRequest,
|
||||
) -> kb_core::Result<crate::Token2022CryptographicPreflightReport> {
|
||||
let requirements = match validate_proof_context_requirements(request) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mut context_slot = request.min_context_slot.unwrap_or(0);
|
||||
let mut reports = std::vec::Vec::with_capacity(requirements.len());
|
||||
for requirement in requirements {
|
||||
let expected_size = requirement.proof_type.context_state_size();
|
||||
let config = match kb_rpc::GetAccountInfoConfig::new_with_data(
|
||||
kb_rpc::RpcCommitmentLevel::Confirmed,
|
||||
request.min_context_slot,
|
||||
expected_size,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let result = match pool
|
||||
.get_account_info_for_role(request.query_role.as_str(), &requirement.account, &config)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let report = match validate_proof_context_account(&requirement, &result) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
context_slot = context_slot.max(report.context_slot);
|
||||
reports.push(report);
|
||||
}
|
||||
return std::result::Result::Ok(crate::Token2022CryptographicPreflightReport {
|
||||
commitment: "confirmed".to_string(),
|
||||
context_slot,
|
||||
proof_contexts: reports,
|
||||
});
|
||||
}
|
||||
|
||||
fn validate_proof_context_requirements(
|
||||
request: &crate::Token2022CryptographicPreflightRequest,
|
||||
) -> kb_core::Result<std::vec::Vec<crate::Token2022ProofContextRequirement>> {
|
||||
if request.query_role.trim().is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Token-2022 cryptographic preflight query_role must not be empty",
|
||||
));
|
||||
}
|
||||
if request.proof_contexts.len() > crate::MAX_TOKEN_2022_PROOF_CONTEXTS {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_proof_context_limit_exceeded",
|
||||
format!(
|
||||
"Token-2022 cryptographic preflight accepts at most {} proof contexts",
|
||||
crate::MAX_TOKEN_2022_PROOF_CONTEXTS
|
||||
),
|
||||
));
|
||||
}
|
||||
let mut indexes = std::collections::BTreeMap::<std::string::String, usize>::new();
|
||||
let mut unique = std::vec::Vec::<crate::Token2022ProofContextRequirement>::new();
|
||||
for requirement in &request.proof_contexts {
|
||||
if requirement.role.trim().is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Token-2022 proof context role must not be empty",
|
||||
));
|
||||
}
|
||||
if let std::option::Option::Some(index) = indexes.get(requirement.account.0.as_str()) {
|
||||
if &unique[*index] != requirement {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_proof_context_conflicting_duplicate",
|
||||
format!(
|
||||
"Token-2022 proof context {} has conflicting requirements",
|
||||
requirement.account.0
|
||||
),
|
||||
));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
indexes.insert(requirement.account.0.clone(), unique.len());
|
||||
unique.push(requirement.clone());
|
||||
}
|
||||
return std::result::Result::Ok(unique);
|
||||
}
|
||||
|
||||
fn validate_proof_context_account(
|
||||
requirement: &crate::Token2022ProofContextRequirement,
|
||||
result: &kb_rpc::AccountInfoResult,
|
||||
) -> kb_core::Result<crate::Token2022ProofContextReport> {
|
||||
let account = match result.account.as_ref() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_proof_context_missing",
|
||||
format!(
|
||||
"Token-2022 proof context account {} does not exist",
|
||||
requirement.account.0
|
||||
),
|
||||
));
|
||||
},
|
||||
};
|
||||
if account.owner.0 != kb_program_ids::ZK_ELGAMAL_PROOF_PROGRAM_ID {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_proof_context_owner_mismatch",
|
||||
format!(
|
||||
"Token-2022 proof context owner must be {}, got {}",
|
||||
kb_program_ids::ZK_ELGAMAL_PROOF_PROGRAM_ID,
|
||||
account.owner.0
|
||||
),
|
||||
));
|
||||
}
|
||||
if account.executable {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_proof_context_executable",
|
||||
"Token-2022 proof context account must not be executable",
|
||||
));
|
||||
}
|
||||
let expected_size = requirement.proof_type.context_state_size();
|
||||
if account.space != expected_size as u64 || account.data.len() != expected_size {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_proof_context_size_mismatch",
|
||||
format!(
|
||||
"Token-2022 proof context {} must contain exactly {expected_size} bytes, got space {} and data {}",
|
||||
requirement.account.0,
|
||||
account.space,
|
||||
account.data.len()
|
||||
),
|
||||
));
|
||||
}
|
||||
if account.data.len() < crate::ZK_PROOF_CONTEXT_META_BYTES {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(
|
||||
"Token-2022 proof context is shorter than its generic metadata",
|
||||
));
|
||||
}
|
||||
let discriminator = account.data[32];
|
||||
if discriminator != requirement.proof_type.discriminator() {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_proof_context_type_mismatch",
|
||||
format!(
|
||||
"Token-2022 proof context {} discriminator {} does not match expected {}",
|
||||
requirement.account.0,
|
||||
discriminator,
|
||||
requirement.proof_type.discriminator()
|
||||
),
|
||||
));
|
||||
}
|
||||
let mut checks = std::vec![
|
||||
"zk_program_owner".to_string(),
|
||||
"not_executable".to_string(),
|
||||
"exact_context_state_size".to_string(),
|
||||
"proof_type_discriminator".to_string(),
|
||||
];
|
||||
if let std::option::Option::Some(expected_authority) = requirement.expected_authority.as_ref() {
|
||||
let retained_authority = bs58::encode(&account.data[..32]).into_string();
|
||||
if retained_authority != expected_authority.0 {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_proof_context_authority_mismatch",
|
||||
format!(
|
||||
"Token-2022 proof context {} retains authority {}, expected {}",
|
||||
requirement.account.0, retained_authority, expected_authority.0
|
||||
),
|
||||
));
|
||||
}
|
||||
checks.push("retained_authority".to_string());
|
||||
}
|
||||
return std::result::Result::Ok(crate::Token2022ProofContextReport {
|
||||
role: requirement.role.clone(),
|
||||
account: requirement.account.clone(),
|
||||
proof_discriminator: discriminator,
|
||||
context_state_bytes: expected_size,
|
||||
context_slot: result.context.slot,
|
||||
checks,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
fn pubkey(byte: u8) -> kb_model::Pubkey {
|
||||
return kb_model::Pubkey(bs58::encode([byte; 32]).into_string());
|
||||
}
|
||||
|
||||
fn requirement(
|
||||
account: kb_model::Pubkey,
|
||||
proof_type: kb_executor_solana_core::ZkElGamalProofType,
|
||||
) -> crate::Token2022ProofContextRequirement {
|
||||
return crate::Token2022ProofContextRequirement {
|
||||
role: "equality_proof".to_string(),
|
||||
account,
|
||||
proof_type,
|
||||
expected_authority: std::option::Option::Some(pubkey(9)),
|
||||
};
|
||||
}
|
||||
|
||||
fn account_result(
|
||||
proof_type: kb_executor_solana_core::ZkElGamalProofType,
|
||||
) -> kb_rpc::AccountInfoResult {
|
||||
let mut data = std::vec![0u8; proof_type.context_state_size()];
|
||||
data[..32].copy_from_slice(&[9u8; 32]);
|
||||
data[32] = proof_type.discriminator();
|
||||
return kb_rpc::AccountInfoResult {
|
||||
context: kb_rpc::RpcResponseContext {
|
||||
slot: 77,
|
||||
api_version: std::option::Option::None,
|
||||
},
|
||||
account: std::option::Option::Some(kb_rpc::AccountInfoValue {
|
||||
lamports: 1,
|
||||
owner: kb_model::ProgramId(kb_program_ids::ZK_ELGAMAL_PROOF_PROGRAM_ID.to_string()),
|
||||
executable: false,
|
||||
rent_epoch: 0,
|
||||
space: data.len() as u64,
|
||||
data,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_duplicates_are_deduplicated_and_conflicts_fail_closed() {
|
||||
let item = requirement(
|
||||
pubkey(1),
|
||||
kb_executor_solana_core::ZkElGamalProofType::CiphertextCiphertextEquality,
|
||||
);
|
||||
let request = crate::Token2022CryptographicPreflightRequest {
|
||||
query_role: "query".to_string(),
|
||||
min_context_slot: std::option::Option::Some(7),
|
||||
proof_contexts: std::vec![item.clone(), item.clone()],
|
||||
};
|
||||
let unique = crate::solana_token_2022_crypto_preflight::validate_proof_context_requirements(
|
||||
&request,
|
||||
);
|
||||
assert_eq!(unique.as_ref().map(std::vec::Vec::len), std::result::Result::Ok(1));
|
||||
let conflict = crate::Token2022CryptographicPreflightRequest {
|
||||
query_role: "query".to_string(),
|
||||
min_context_slot: std::option::Option::None,
|
||||
proof_contexts: std::vec![
|
||||
item,
|
||||
requirement(
|
||||
pubkey(1),
|
||||
kb_executor_solana_core::ZkElGamalProofType::BatchedRangeProofU128,
|
||||
),
|
||||
],
|
||||
};
|
||||
assert!(
|
||||
crate::solana_token_2022_crypto_preflight::validate_proof_context_requirements(
|
||||
&conflict
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proof_context_validation_checks_owner_size_type_and_authority() {
|
||||
let proof_type = kb_executor_solana_core::ZkElGamalProofType::CiphertextCiphertextEquality;
|
||||
let context_requirement = requirement(pubkey(1), proof_type);
|
||||
let result = account_result(proof_type);
|
||||
let report = crate::solana_token_2022_crypto_preflight::validate_proof_context_account(
|
||||
&context_requirement,
|
||||
&result,
|
||||
);
|
||||
assert_eq!(
|
||||
report.as_ref().map(|value| return value.checks.len()),
|
||||
std::result::Result::Ok(5)
|
||||
);
|
||||
let wrong_type = requirement(
|
||||
pubkey(1),
|
||||
kb_executor_solana_core::ZkElGamalProofType::BatchedRangeProofU128,
|
||||
);
|
||||
assert!(
|
||||
crate::solana_token_2022_crypto_preflight::validate_proof_context_account(
|
||||
&wrong_type,
|
||||
&result,
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,968 @@
|
||||
// file: kb_pipeline/src/solana_token_2022_devnet_execution.rs
|
||||
// version: 2
|
||||
|
||||
//! Devnet Token-2022 execution with stateful and canonical post-validation.
|
||||
|
||||
/// Complete request for one Devnet Token-2022 execution.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct DevnetSplToken2022ExecutionRequest {
|
||||
/// Stable caller-provided execution identifier.
|
||||
pub intent_id: std::string::String,
|
||||
/// Endpoint role used for state, balance, blockhash, fee and hydration calls.
|
||||
pub query_role: std::string::String,
|
||||
/// Endpoint role used for simulation, submission and confirmation polling.
|
||||
pub transaction_role: std::string::String,
|
||||
/// Exact typed Token-2022 operation.
|
||||
pub operation: kb_executor_spl_token_2022::SplToken2022Operation,
|
||||
/// Explicitly authorizes signing and submission after successful simulation.
|
||||
pub submit: bool,
|
||||
/// Explicit operator confirmation required by the active profile.
|
||||
pub operator_confirmed: bool,
|
||||
/// Number of `getTransaction` retries after the first hydration attempt.
|
||||
pub post_validation_max_retries: u32,
|
||||
/// Replaces existing core and decode outputs for the submitted signature.
|
||||
pub force_post_validation_replay: bool,
|
||||
}
|
||||
|
||||
impl crate::solana_token_2022_devnet_execution::DevnetSplToken2022ExecutionRequest {
|
||||
/// Creates a conservative simulation-only request.
|
||||
pub fn new(
|
||||
intent_id: impl std::convert::Into<std::string::String>,
|
||||
operation: kb_executor_spl_token_2022::SplToken2022Operation,
|
||||
) -> Self {
|
||||
return Self {
|
||||
intent_id: intent_id.into(),
|
||||
query_role: "http_queries".to_string(),
|
||||
transaction_role: "http_transactions".to_string(),
|
||||
operation,
|
||||
submit: false,
|
||||
operator_confirmed: false,
|
||||
post_validation_max_retries: 10,
|
||||
force_post_validation_replay: false,
|
||||
};
|
||||
}
|
||||
|
||||
/// Validates request-local bounds independently from one profile.
|
||||
pub fn validate(&self) -> kb_core::Result<()> {
|
||||
if self.intent_id.trim().is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Devnet Token-2022 execution intent id must not be empty",
|
||||
));
|
||||
}
|
||||
if self.query_role.trim().is_empty() || self.transaction_role.trim().is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Devnet Token-2022 execution endpoint roles must not be empty",
|
||||
));
|
||||
}
|
||||
if self.post_validation_max_retries > 20 {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"post-execution getTransaction retries must not exceed 20",
|
||||
));
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
/// Complete result of one Devnet Token-2022 execution.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct DevnetSplToken2022ExecutionSummary {
|
||||
/// Profile used by the orchestration.
|
||||
pub profile_name: std::string::String,
|
||||
/// Exact classified cluster.
|
||||
pub cluster: kb_execution_api::ExecutionCluster,
|
||||
/// Genesis hash returned by the selected endpoint.
|
||||
pub genesis_hash: std::string::String,
|
||||
/// Non-secret persistent wallet description.
|
||||
pub wallet: kb_wallet::WalletSummary,
|
||||
/// Wallet balance observed before planning.
|
||||
pub balance_lamports: u64,
|
||||
/// Stateful readiness report produced before plan simulation.
|
||||
pub stateful_preflight: crate::Token2022PreflightReport,
|
||||
/// Exact prepared Token plan.
|
||||
pub plan: kb_execution_api::PreparedExecutionPlan,
|
||||
/// Recent blockhash used by the exact transaction.
|
||||
pub latest_blockhash: kb_rpc::LatestBlockhashResult,
|
||||
/// Fee estimate for the exact compiled message.
|
||||
pub fee: kb_rpc::FeeForMessageResult,
|
||||
/// Exact simulation result bound to the compiled message.
|
||||
pub simulation: kb_execution_api::ExecutionSimulationResult,
|
||||
/// Submission result when explicitly authorized.
|
||||
pub send_result: std::option::Option<kb_execution_api::ExecutionSendResult>,
|
||||
/// Confirmation result when submitted.
|
||||
pub confirmation: std::option::Option<kb_execution_api::ExecutionConfirmationResult>,
|
||||
/// Canonical hydration result for the exact signature.
|
||||
pub backfill: std::option::Option<crate::BackfillSummary>,
|
||||
/// Core extraction result for the exact signature.
|
||||
pub core_extraction: std::option::Option<crate::CoreExtractionSummary>,
|
||||
/// First Token decode and materialization replay.
|
||||
pub decode_replay: std::option::Option<crate::DecodeReplaySummary>,
|
||||
/// Second replay proving idempotence for the same decoder version and input.
|
||||
pub idempotence_replay: std::option::Option<crate::DecodeReplaySummary>,
|
||||
/// Exact materialized rows produced for the submitted Token transaction.
|
||||
pub materializations: std::vec::Vec<kb_store_core::MaterializedEventQueryRow>,
|
||||
/// Aggregated post-execution validation diagnostic.
|
||||
pub post_execution: std::option::Option<kb_execution_api::PostExecutionDiagnostic>,
|
||||
}
|
||||
|
||||
struct PreparedToken2022Execution {
|
||||
wallet: kb_wallet::TemporaryWallet,
|
||||
unsigned: kb_execution_solana::UnsignedSolanaTransaction,
|
||||
evidence: kb_execution_solana::SolanaSimulationEvidence,
|
||||
summary: crate::solana_token_2022_devnet_execution::DevnetSplToken2022ExecutionSummary,
|
||||
}
|
||||
|
||||
/// Simulates one Devnet Token-2022 operation after stateful preflight.
|
||||
pub async fn simulate_devnet_spl_token_2022<O>(
|
||||
http_pool: &kb_rpc::HttpEndpointPool,
|
||||
profile: &kb_config::ProfileConfig,
|
||||
workspace_root: &std::path::Path,
|
||||
request: &crate::solana_token_2022_devnet_execution::DevnetSplToken2022ExecutionRequest,
|
||||
observer: &O,
|
||||
) -> kb_core::Result<crate::solana_token_2022_devnet_execution::DevnetSplToken2022ExecutionSummary>
|
||||
where
|
||||
O: crate::SolanaExecutionObserver,
|
||||
{
|
||||
if request.submit {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"simulate_devnet_spl_token_2022 requires submit=false",
|
||||
));
|
||||
}
|
||||
let prepared = match crate::solana_token_2022_devnet_execution::prepare_execution(
|
||||
http_pool,
|
||||
profile,
|
||||
workspace_root,
|
||||
request,
|
||||
observer,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(prepared.summary);
|
||||
}
|
||||
|
||||
/// Executes one Devnet Token-2022 simulation or authorized submission.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn execute_devnet_spl_token_2022<S, O>(
|
||||
http_pool: &kb_rpc::HttpEndpointPool,
|
||||
store: &S,
|
||||
profile: &kb_config::ProfileConfig,
|
||||
workspace_root: &std::path::Path,
|
||||
request: &crate::solana_token_2022_devnet_execution::DevnetSplToken2022ExecutionRequest,
|
||||
decoders: &[std::sync::Arc<dyn kb_decoder_api::InstructionDecoder>],
|
||||
materializers: &[std::sync::Arc<dyn kb_materializer_api::EventMaterializer>],
|
||||
observer: &O,
|
||||
) -> kb_core::Result<crate::solana_token_2022_devnet_execution::DevnetSplToken2022ExecutionSummary>
|
||||
where
|
||||
S: kb_store_core::RawTransactionStore
|
||||
+ kb_store_core::CoreExtractionStore
|
||||
+ kb_store_core::DecodePipelineStore
|
||||
+ Sync,
|
||||
O: crate::SolanaExecutionObserver,
|
||||
{
|
||||
let prepared = match crate::solana_token_2022_devnet_execution::prepare_execution(
|
||||
http_pool,
|
||||
profile,
|
||||
workspace_root,
|
||||
request,
|
||||
observer,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if !request.submit {
|
||||
return std::result::Result::Ok(prepared.summary);
|
||||
}
|
||||
if !prepared.summary.simulation.success {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_simulation_failed",
|
||||
crate::solana_execution::simulation_failure_message(&prepared.summary.simulation),
|
||||
));
|
||||
}
|
||||
if let std::result::Result::Err(error) =
|
||||
crate::solana_token_2022_devnet_execution::validate_profile_wallet_signers(
|
||||
prepared.unsigned.required_signer_pubkeys(),
|
||||
prepared.summary.wallet.public_key.as_str(),
|
||||
)
|
||||
{
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let send_evaluation = match kb_execution_safety::ExecutionSafetyChecker
|
||||
.evaluate_send(&prepared.summary.plan, &prepared.summary.simulation)
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if send_evaluation.decision == kb_execution_safety::ExecutionSafetyDecision::Deny {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_send_denied",
|
||||
crate::solana_execution::violation_message(send_evaluation.violations.as_slice()),
|
||||
));
|
||||
}
|
||||
let signed = match prepared
|
||||
.unsigned
|
||||
.sign_after_simulation(&prepared.evidence, &[prepared.wallet.as_signer()])
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if let std::result::Result::Err(error) = signed.verify_signatures() {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let signature = signed.primary_signature().clone();
|
||||
let mut summary = prepared.summary;
|
||||
let mut diagnostic = kb_execution_api::PostExecutionDiagnostic {
|
||||
signature: signature.clone(),
|
||||
canonical_inserted: false,
|
||||
core_extracted: false,
|
||||
decode_replayed: false,
|
||||
materialized: false,
|
||||
diagnostics: std::vec::Vec::new(),
|
||||
};
|
||||
let send_config = match kb_rpc::SendTransactionConfig::from_execution_config(
|
||||
&profile.execution,
|
||||
std::option::Option::Some(summary.latest_blockhash.context.slot),
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let sent = match http_pool
|
||||
.send_transaction_for_role(
|
||||
request.transaction_role.as_str(),
|
||||
signed.transaction_base64().as_str(),
|
||||
&signature,
|
||||
&send_config,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
diagnostic.diagnostics.push(format!("Token-2022 submission failed: {error}"));
|
||||
summary.post_execution = std::option::Option::Some(diagnostic);
|
||||
return std::result::Result::Ok(summary);
|
||||
},
|
||||
};
|
||||
summary.send_result = std::option::Option::Some(
|
||||
sent.to_execution_result(kb_execution_api::ExecutionCluster::Devnet),
|
||||
);
|
||||
let confirmation_config = match kb_rpc::ConfirmTransactionConfig::from_execution_config(
|
||||
&profile.execution,
|
||||
std::option::Option::Some(summary.latest_blockhash.last_valid_block_height),
|
||||
std::option::Option::Some(summary.latest_blockhash.context.slot),
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let confirmation = match http_pool
|
||||
.confirm_transaction_for_roles(
|
||||
request.transaction_role.as_str(),
|
||||
request.query_role.as_str(),
|
||||
kb_execution_api::ExecutionCluster::Devnet,
|
||||
&signature,
|
||||
&confirmation_config,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
diagnostic.diagnostics.push(format!("Token-2022 confirmation failed: {error}"));
|
||||
summary.post_execution = std::option::Option::Some(diagnostic);
|
||||
return std::result::Result::Ok(summary);
|
||||
},
|
||||
};
|
||||
let confirmation_status = confirmation.status;
|
||||
summary.confirmation = std::option::Option::Some(confirmation);
|
||||
if !matches!(
|
||||
confirmation_status,
|
||||
kb_execution_api::ExecutionConfirmationStatus::Confirmed
|
||||
| kb_execution_api::ExecutionConfirmationStatus::Finalized
|
||||
) {
|
||||
diagnostic.diagnostics.push(format!(
|
||||
"Token-2022 post-validation stopped at confirmation status {confirmation_status:?}"
|
||||
));
|
||||
summary.post_execution = std::option::Option::Some(diagnostic);
|
||||
return std::result::Result::Ok(summary);
|
||||
}
|
||||
let backfill = match crate::solana_token_execution::hydrate_signature(
|
||||
http_pool,
|
||||
store,
|
||||
profile,
|
||||
request.query_role.as_str(),
|
||||
request.post_validation_max_retries,
|
||||
observer,
|
||||
&signature,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
diagnostic.diagnostics.push(format!("Token-2022 hydration failed: {error}"));
|
||||
summary.post_execution = std::option::Option::Some(diagnostic);
|
||||
return std::result::Result::Ok(summary);
|
||||
},
|
||||
};
|
||||
diagnostic.canonical_inserted = crate::solana_token_execution::canonical_available(&backfill);
|
||||
summary.backfill = std::option::Option::Some(backfill);
|
||||
if !diagnostic.canonical_inserted {
|
||||
diagnostic.diagnostics.push(
|
||||
"confirmed Token-2022 transaction was unavailable for canonical hydration".to_string(),
|
||||
);
|
||||
summary.post_execution = std::option::Option::Some(diagnostic);
|
||||
return std::result::Result::Ok(summary);
|
||||
}
|
||||
let extraction = match crate::execute_core_extraction(
|
||||
store,
|
||||
&crate::CoreExtractionRequest {
|
||||
source: crate::CoreExtractionSource::Signatures(std::vec![signature.0.clone()]),
|
||||
limit: 1,
|
||||
max_concurrent_extractions: 1,
|
||||
force_replay: request.force_post_validation_replay,
|
||||
},
|
||||
observer,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
diagnostic
|
||||
.diagnostics
|
||||
.push(format!("Token-2022 core extraction failed: {error}"));
|
||||
summary.post_execution = std::option::Option::Some(diagnostic);
|
||||
return std::result::Result::Ok(summary);
|
||||
},
|
||||
};
|
||||
diagnostic.core_extracted = extraction.failed == 0
|
||||
&& !extraction.cancelled
|
||||
&& extraction.selected == 1
|
||||
&& extraction.extracted.saturating_add(extraction.skipped) >= 1;
|
||||
summary.core_extraction = std::option::Option::Some(extraction);
|
||||
if !diagnostic.core_extracted {
|
||||
diagnostic
|
||||
.diagnostics
|
||||
.push("Token-2022 core extraction did not complete".to_string());
|
||||
summary.post_execution = std::option::Option::Some(diagnostic);
|
||||
return std::result::Result::Ok(summary);
|
||||
}
|
||||
let first_replay = match crate::solana_token_execution::replay_program(
|
||||
store,
|
||||
&signature,
|
||||
false,
|
||||
request.force_post_validation_replay,
|
||||
&[kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID],
|
||||
decoders,
|
||||
materializers,
|
||||
observer,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
diagnostic.diagnostics.push(format!("Token-2022 decode replay failed: {error}"));
|
||||
summary.post_execution = std::option::Option::Some(diagnostic);
|
||||
return std::result::Result::Ok(summary);
|
||||
},
|
||||
};
|
||||
diagnostic.decode_replayed = crate::solana_token_execution::decode_completed(&first_replay);
|
||||
summary.decode_replay = std::option::Option::Some(first_replay);
|
||||
let filter = match kb_store_core::MaterializedEventFilter::new(
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
std::option::Option::Some(signature.0.clone()),
|
||||
64,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let rows =
|
||||
match kb_store_core::DecodePipelineStore::list_materialized_events(store, &filter).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
diagnostic
|
||||
.diagnostics
|
||||
.push(format!("Token-2022 materialization query failed: {error}"));
|
||||
summary.post_execution = std::option::Option::Some(diagnostic);
|
||||
return std::result::Result::Ok(summary);
|
||||
},
|
||||
};
|
||||
summary.materializations = rows
|
||||
.into_iter()
|
||||
.filter(|row| return row.source_decoder_name == "spl_token_2022")
|
||||
.collect();
|
||||
diagnostic.materialized =
|
||||
!summary.plan.policy.post_execution_validation.materialization_required
|
||||
|| !summary.materializations.is_empty();
|
||||
let second_replay = match crate::solana_token_execution::replay_program(
|
||||
store,
|
||||
&signature,
|
||||
true,
|
||||
request.force_post_validation_replay,
|
||||
&[kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID],
|
||||
decoders,
|
||||
materializers,
|
||||
observer,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
diagnostic
|
||||
.diagnostics
|
||||
.push(format!("Token-2022 idempotence replay failed: {error}"));
|
||||
summary.post_execution = std::option::Option::Some(diagnostic);
|
||||
return std::result::Result::Ok(summary);
|
||||
},
|
||||
};
|
||||
let idempotent = second_replay.failed_inputs == 0
|
||||
&& second_replay.processors.iter().all(|processor| {
|
||||
return processor.failed == 0
|
||||
&& processor.materialized_outputs == 0
|
||||
&& processor.materialization_refused == 0;
|
||||
});
|
||||
summary.idempotence_replay = std::option::Option::Some(second_replay);
|
||||
if !idempotent {
|
||||
diagnostic
|
||||
.diagnostics
|
||||
.push("second Token-2022 replay did not prove a clean idempotent skip".to_string());
|
||||
} else if diagnostic.canonical_inserted
|
||||
&& diagnostic.core_extracted
|
||||
&& diagnostic.decode_replayed
|
||||
&& diagnostic.materialized
|
||||
{
|
||||
diagnostic.diagnostics.push(
|
||||
"Token-2022 completed canonical hydration, core extraction, decode, materialization and idempotence validation"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
summary.post_execution = std::option::Option::Some(diagnostic);
|
||||
return std::result::Result::Ok(summary);
|
||||
}
|
||||
|
||||
async fn prepare_execution<O>(
|
||||
http_pool: &kb_rpc::HttpEndpointPool,
|
||||
profile: &kb_config::ProfileConfig,
|
||||
workspace_root: &std::path::Path,
|
||||
request: &crate::solana_token_2022_devnet_execution::DevnetSplToken2022ExecutionRequest,
|
||||
observer: &O,
|
||||
) -> kb_core::Result<crate::solana_token_2022_devnet_execution::PreparedToken2022Execution>
|
||||
where
|
||||
O: crate::SolanaExecutionObserver,
|
||||
{
|
||||
if let std::result::Result::Err(error) = request.validate() {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
if let std::result::Result::Err(error) =
|
||||
crate::solana_token_2022_devnet_execution::validate_profile(profile, request)
|
||||
{
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
if let std::result::Result::Err(error) =
|
||||
crate::solana_execution::ensure_not_cancelled(observer, "token_validate")
|
||||
{
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let genesis = match http_pool.get_genesis_hash_for_role(request.query_role.as_str()).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if genesis.classified_cluster
|
||||
!= std::option::Option::Some(kb_execution_api::ExecutionCluster::Devnet)
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_cluster_mismatch",
|
||||
format!(
|
||||
"expected Devnet genesis hash but endpoint returned {} classified as {:?}",
|
||||
genesis.genesis_hash, genesis.classified_cluster
|
||||
),
|
||||
));
|
||||
}
|
||||
let preflight_request =
|
||||
match crate::solana_token_2022_devnet_execution::preflight_request(request) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let readiness = match crate::inspect_token_2022_preflight(http_pool, &preflight_request).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let wallet = match crate::solana_execution::load_profile_wallet(profile, workspace_root).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let wallet_summary = wallet.summary();
|
||||
let fee_payer = kb_model::Pubkey(wallet_summary.public_key.clone());
|
||||
let balance = match http_pool
|
||||
.get_balance_for_role(
|
||||
request.query_role.as_str(),
|
||||
&fee_payer,
|
||||
&kb_rpc::GetBalanceConfig::confirmed(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if balance.lamports < profile.execution.max_fee_lamports {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_balance_insufficient",
|
||||
format!(
|
||||
"Devnet wallet balance {} is below the configured fee ceiling {}",
|
||||
balance.lamports, profile.execution.max_fee_lamports
|
||||
),
|
||||
));
|
||||
}
|
||||
let plan = match crate::solana_token_2022_devnet_execution::build_plan(
|
||||
profile,
|
||||
request,
|
||||
fee_payer.clone(),
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let plan_evaluation =
|
||||
match kb_execution_safety::ExecutionSafetyChecker.evaluate_prepared_plan(&plan) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if plan_evaluation.decision == kb_execution_safety::ExecutionSafetyDecision::Deny {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_plan_denied",
|
||||
crate::solana_execution::violation_message(plan_evaluation.violations.as_slice()),
|
||||
));
|
||||
}
|
||||
let latest_blockhash = match http_pool
|
||||
.get_latest_blockhash_for_role(
|
||||
request.query_role.as_str(),
|
||||
&kb_rpc::GetLatestBlockhashConfig::confirmed(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let unsigned = match kb_execution_solana::build_legacy_transaction(
|
||||
&plan,
|
||||
latest_blockhash.blockhash.as_str(),
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let fee = match http_pool
|
||||
.get_fee_for_message_for_role(
|
||||
request.query_role.as_str(),
|
||||
unsigned.message_base64().as_str(),
|
||||
&kb_rpc::GetFeeForMessageConfig::new(
|
||||
kb_rpc::RpcCommitmentLevel::Confirmed,
|
||||
std::option::Option::Some(latest_blockhash.context.slot),
|
||||
),
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if fee.fee_lamports.is_none() {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_fee_unavailable",
|
||||
"getFeeForMessage returned null for the selected recent blockhash",
|
||||
));
|
||||
}
|
||||
let unsigned_base64 = match unsigned.transaction_base64() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let simulation_config = match kb_rpc::SimulateTransactionConfig::new(
|
||||
kb_rpc::RpcCommitmentLevel::Confirmed,
|
||||
false,
|
||||
false,
|
||||
std::option::Option::Some(latest_blockhash.context.slot),
|
||||
true,
|
||||
std::option::Option::None,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
crate::solana_execution::emit(
|
||||
observer,
|
||||
crate::SolanaExecutionProgressLevel::Info,
|
||||
"spl_token_2022_simulation",
|
||||
format!("simulating exact Token-2022 message {}", unsigned.message_hash()),
|
||||
std::option::Option::None,
|
||||
);
|
||||
let simulation_rpc = match http_pool
|
||||
.simulate_transaction_for_role(
|
||||
request.transaction_role.as_str(),
|
||||
unsigned_base64.as_str(),
|
||||
&simulation_config,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let simulation = simulation_rpc.to_execution_result(
|
||||
kb_execution_api::ExecutionCluster::Devnet,
|
||||
kb_execution_api::ExecutionBlockhashKind::Latest,
|
||||
std::option::Option::Some(
|
||||
simulation_rpc.context.slot.saturating_sub(latest_blockhash.context.slot),
|
||||
),
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
std::option::Option::Some(&fee),
|
||||
);
|
||||
let evidence = unsigned.bind_simulation(simulation.clone());
|
||||
let summary = crate::solana_token_2022_devnet_execution::DevnetSplToken2022ExecutionSummary {
|
||||
profile_name: profile.name.clone(),
|
||||
cluster: kb_execution_api::ExecutionCluster::Devnet,
|
||||
genesis_hash: genesis.genesis_hash,
|
||||
wallet: wallet_summary,
|
||||
balance_lamports: balance.lamports,
|
||||
stateful_preflight: readiness,
|
||||
plan,
|
||||
latest_blockhash,
|
||||
fee,
|
||||
simulation,
|
||||
send_result: std::option::Option::None,
|
||||
confirmation: std::option::Option::None,
|
||||
backfill: std::option::Option::None,
|
||||
core_extraction: std::option::Option::None,
|
||||
decode_replay: std::option::Option::None,
|
||||
idempotence_replay: std::option::Option::None,
|
||||
materializations: std::vec::Vec::new(),
|
||||
post_execution: std::option::Option::None,
|
||||
};
|
||||
return std::result::Result::Ok(
|
||||
crate::solana_token_2022_devnet_execution::PreparedToken2022Execution {
|
||||
wallet,
|
||||
unsigned,
|
||||
evidence,
|
||||
summary,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn preflight_request(
|
||||
request: &crate::solana_token_2022_devnet_execution::DevnetSplToken2022ExecutionRequest,
|
||||
) -> kb_core::Result<crate::Token2022PreflightRequest> {
|
||||
let operation = match &request.operation {
|
||||
kb_executor_spl_token_2022::SplToken2022Operation::Instruction { value } => value.as_ref(),
|
||||
kb_executor_spl_token_2022::SplToken2022Operation::Batch { instructions: _ } => {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_spl_token_2022_batch_not_supported",
|
||||
"Devnet Token-2022 validation scenarios require one non-batch operation",
|
||||
));
|
||||
},
|
||||
};
|
||||
let mut requirements = std::vec::Vec::new();
|
||||
match operation {
|
||||
kb_executor_spl_token_2022::SplTokenSingleOperation::MintToChecked {
|
||||
mint,
|
||||
destination,
|
||||
authority,
|
||||
amount: _,
|
||||
decimals,
|
||||
} => {
|
||||
requirements.push(mint_requirement("mint", mint, std::option::Option::Some(*decimals)));
|
||||
requirements.push(account_requirement(
|
||||
"destination",
|
||||
destination,
|
||||
mint,
|
||||
std::option::Option::None,
|
||||
));
|
||||
if let std::result::Result::Err(error) = validate_single_authority(authority) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
},
|
||||
kb_executor_spl_token_2022::SplTokenSingleOperation::TransferChecked {
|
||||
source,
|
||||
mint,
|
||||
destination,
|
||||
authority,
|
||||
amount: _,
|
||||
decimals,
|
||||
} => {
|
||||
requirements.push(account_requirement(
|
||||
"source",
|
||||
source,
|
||||
mint,
|
||||
std::option::Option::Some(authority.authority.clone()),
|
||||
));
|
||||
requirements.push(mint_requirement("mint", mint, std::option::Option::Some(*decimals)));
|
||||
requirements.push(account_requirement(
|
||||
"destination",
|
||||
destination,
|
||||
mint,
|
||||
std::option::Option::None,
|
||||
));
|
||||
if let std::result::Result::Err(error) = validate_single_authority(authority) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
},
|
||||
kb_executor_spl_token_2022::SplTokenSingleOperation::ApproveChecked {
|
||||
source,
|
||||
mint,
|
||||
delegate: _,
|
||||
authority,
|
||||
amount: _,
|
||||
decimals,
|
||||
} => {
|
||||
requirements.push(account_requirement(
|
||||
"source",
|
||||
source,
|
||||
mint,
|
||||
std::option::Option::Some(authority.authority.clone()),
|
||||
));
|
||||
requirements.push(mint_requirement("mint", mint, std::option::Option::Some(*decimals)));
|
||||
if let std::result::Result::Err(error) = validate_single_authority(authority) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
},
|
||||
kb_executor_spl_token_2022::SplTokenSingleOperation::Revoke { source, authority } => {
|
||||
requirements.push(account_requirement(
|
||||
"source",
|
||||
source,
|
||||
&kb_model::Pubkey(std::string::String::new()),
|
||||
std::option::Option::Some(authority.authority.clone()),
|
||||
));
|
||||
requirements[0].expected_mint = std::option::Option::None;
|
||||
if let std::result::Result::Err(error) = validate_single_authority(authority) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
},
|
||||
kb_executor_spl_token_2022::SplTokenSingleOperation::BurnChecked {
|
||||
source,
|
||||
mint,
|
||||
authority,
|
||||
amount: _,
|
||||
decimals,
|
||||
} => {
|
||||
requirements.push(account_requirement(
|
||||
"source",
|
||||
source,
|
||||
mint,
|
||||
std::option::Option::Some(authority.authority.clone()),
|
||||
));
|
||||
requirements.push(mint_requirement("mint", mint, std::option::Option::Some(*decimals)));
|
||||
if let std::result::Result::Err(error) = validate_single_authority(authority) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
},
|
||||
kb_executor_spl_token_2022::SplTokenSingleOperation::FreezeAccount {
|
||||
account,
|
||||
mint,
|
||||
authority,
|
||||
}
|
||||
| kb_executor_spl_token_2022::SplTokenSingleOperation::ThawAccount {
|
||||
account,
|
||||
mint,
|
||||
authority,
|
||||
} => {
|
||||
requirements.push(account_requirement(
|
||||
"account",
|
||||
account,
|
||||
mint,
|
||||
std::option::Option::None,
|
||||
));
|
||||
requirements.push(mint_requirement("mint", mint, std::option::Option::None));
|
||||
if let std::result::Result::Err(error) = validate_single_authority(authority) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
},
|
||||
kb_executor_spl_token_2022::SplTokenSingleOperation::CloseAccount {
|
||||
account,
|
||||
destination: _,
|
||||
authority,
|
||||
} => {
|
||||
requirements.push(account_requirement(
|
||||
"account",
|
||||
account,
|
||||
&kb_model::Pubkey(std::string::String::new()),
|
||||
std::option::Option::Some(authority.authority.clone()),
|
||||
));
|
||||
requirements[0].expected_mint = std::option::Option::None;
|
||||
if let std::result::Result::Err(error) = validate_single_authority(authority) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_spl_token_2022_devnet_operation_unsupported",
|
||||
format!(
|
||||
"Token-2022 Devnet validation does not expose {}",
|
||||
operation.operation_code()
|
||||
),
|
||||
));
|
||||
},
|
||||
}
|
||||
return std::result::Result::Ok(crate::Token2022PreflightRequest {
|
||||
query_role: request.query_role.clone(),
|
||||
min_context_slot: std::option::Option::None,
|
||||
max_accounts: crate::MAX_TOKEN_2022_PREFLIGHT_ACCOUNTS,
|
||||
max_total_data_bytes: crate::MAX_TOKEN_2022_PREFLIGHT_TOTAL_BYTES,
|
||||
requirements,
|
||||
elgamal_registry: std::option::Option::None,
|
||||
});
|
||||
}
|
||||
|
||||
fn validate_single_authority(
|
||||
authority: &kb_executor_spl_token_2022::SplTokenAuthority,
|
||||
) -> kb_core::Result<()> {
|
||||
if !authority.multisig_signers.is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_spl_token_2022_devnet_multisig_not_supported",
|
||||
"the Devnet validation UI currently supports one profile-wallet authority",
|
||||
));
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn mint_requirement(
|
||||
role: &str,
|
||||
mint: &kb_model::Pubkey,
|
||||
decimals: std::option::Option<u8>,
|
||||
) -> crate::Token2022PreflightRequirement {
|
||||
return crate::Token2022PreflightRequirement {
|
||||
role: role.to_string(),
|
||||
account: mint.clone(),
|
||||
kind: kb_decoder_spl_token_2022::state::Token2022StateKind::Mint,
|
||||
max_data_bytes: 65_536,
|
||||
expected_mint: std::option::Option::None,
|
||||
expected_owner: std::option::Option::None,
|
||||
expected_decimals: decimals,
|
||||
required_extensions: std::vec::Vec::new(),
|
||||
context: crate::Token2022StatefulContext::default(),
|
||||
};
|
||||
}
|
||||
|
||||
fn account_requirement(
|
||||
role: &str,
|
||||
account: &kb_model::Pubkey,
|
||||
mint: &kb_model::Pubkey,
|
||||
owner: std::option::Option<kb_model::Pubkey>,
|
||||
) -> crate::Token2022PreflightRequirement {
|
||||
return crate::Token2022PreflightRequirement {
|
||||
role: role.to_string(),
|
||||
account: account.clone(),
|
||||
kind: kb_decoder_spl_token_2022::state::Token2022StateKind::Account,
|
||||
max_data_bytes: 65_536,
|
||||
expected_mint: std::option::Option::Some(mint.clone()),
|
||||
expected_owner: owner,
|
||||
expected_decimals: std::option::Option::None,
|
||||
required_extensions: std::vec::Vec::new(),
|
||||
context: crate::Token2022StatefulContext::default(),
|
||||
};
|
||||
}
|
||||
|
||||
fn validate_profile(
|
||||
profile: &kb_config::ProfileConfig,
|
||||
request: &crate::solana_token_2022_devnet_execution::DevnetSplToken2022ExecutionRequest,
|
||||
) -> kb_core::Result<()> {
|
||||
if profile.wallet.cluster != "devnet" {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Token-2022 Devnet orchestration requires a Devnet wallet profile",
|
||||
));
|
||||
}
|
||||
if !profile.wallet.temporary_wallet_enabled || !profile.wallet.temporary_wallet_persist {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Token-2022 Devnet orchestration requires an enabled persistent temporary wallet",
|
||||
));
|
||||
}
|
||||
if !profile.execution.require_simulation {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Token-2022 Devnet orchestration requires simulation",
|
||||
));
|
||||
}
|
||||
if request.submit && !profile.wallet.devnet_send_enabled {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Devnet transaction submission is disabled by the wallet profile",
|
||||
));
|
||||
}
|
||||
if request.submit
|
||||
&& profile.execution.require_operator_confirmation
|
||||
&& !request.operator_confirmed
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Token-2022 Devnet submission requires explicit operator confirmation",
|
||||
));
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn build_plan(
|
||||
profile: &kb_config::ProfileConfig,
|
||||
request: &crate::solana_token_2022_devnet_execution::DevnetSplToken2022ExecutionRequest,
|
||||
fee_payer: kb_model::Pubkey,
|
||||
) -> kb_core::Result<kb_execution_api::PreparedExecutionPlan> {
|
||||
let materialization_required =
|
||||
crate::solana_token_2022_devnet_execution::operation_requires_materialization(
|
||||
&request.operation,
|
||||
);
|
||||
let authorized_signers = std::vec![fee_payer.clone()];
|
||||
let intent = kb_executor_spl_token_2022::SplToken2022ExecutionIntent {
|
||||
intent_id: request.intent_id.clone(),
|
||||
fee_payer,
|
||||
policy: kb_execution_api::ExecutionPolicy {
|
||||
cluster: kb_execution_api::ExecutionClusterPolicy {
|
||||
expected_cluster: kb_execution_api::ExecutionCluster::Devnet,
|
||||
allow_mainnet: false,
|
||||
mainnet_confirmation: false,
|
||||
},
|
||||
simulation: kb_execution_api::ExecutionSimulationPolicy::Required,
|
||||
blockhash: kb_execution_api::ExecutionBlockhashPolicy {
|
||||
kind: kb_execution_api::ExecutionBlockhashKind::Latest,
|
||||
max_age_slots: std::option::Option::Some(
|
||||
profile.execution.recent_blockhash_max_age_slots,
|
||||
),
|
||||
nonce_account: std::option::Option::None,
|
||||
nonce_authority: std::option::Option::None,
|
||||
},
|
||||
cost_limit: kb_execution_api::ExecutionCostLimit {
|
||||
max_spend_lamports: std::option::Option::Some(
|
||||
profile.execution.devnet_max_spend_lamports,
|
||||
),
|
||||
max_fee_lamports: std::option::Option::Some(profile.execution.max_fee_lamports),
|
||||
max_compute_unit_price_micro_lamports: std::option::Option::Some(
|
||||
profile.execution.max_compute_unit_price_micro_lamports,
|
||||
),
|
||||
},
|
||||
authorized_signers,
|
||||
dry_run: !request.submit,
|
||||
post_execution_validation: kb_execution_api::PostExecutionValidationPolicy {
|
||||
canonical_insert_required: true,
|
||||
core_extraction_required: true,
|
||||
decode_replay_required: true,
|
||||
materialization_required,
|
||||
},
|
||||
},
|
||||
operation: request.operation.clone(),
|
||||
};
|
||||
return kb_execution_api::TypedInstructionExecutor::build_prepared_plan(
|
||||
&kb_executor_spl_token_2022::SplToken2022Executor,
|
||||
&intent,
|
||||
);
|
||||
}
|
||||
|
||||
fn operation_requires_materialization(
|
||||
_operation: &kb_executor_spl_token_2022::SplToken2022Operation,
|
||||
) -> bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
fn validate_profile_wallet_signers(
|
||||
required_signers: &[std::string::String],
|
||||
wallet_pubkey: &str,
|
||||
) -> kb_core::Result<()> {
|
||||
if required_signers.iter().any(|value| return value.as_str() != wallet_pubkey) {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_spl_token_2022_external_signer_unavailable",
|
||||
format!(
|
||||
"the Devnet Token orchestrator can sign only with profile wallet {}; required signers are {}",
|
||||
wallet_pubkey,
|
||||
required_signers.join(",")
|
||||
),
|
||||
));
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
// file: kb_pipeline/src/solana_token_2022_devnet_scenarios.rs
|
||||
// version: 2
|
||||
|
||||
//! Stable Devnet validation scenarios required to close milestone 0.4.6.
|
||||
|
||||
/// Stable category of one independent Devnet validation scenario.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DevnetSplValidationFamily {
|
||||
/// Public Token-2022 state mutations without cryptographic proofs.
|
||||
Token2022Public,
|
||||
/// ElGamal Registry lifecycle kept technically separate from Token-2022.
|
||||
ElGamalRegistry,
|
||||
/// Token-2022 confidential operations requiring proof material.
|
||||
Token2022Confidential,
|
||||
}
|
||||
|
||||
/// Stable status of one scenario in the application validation workflow.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DevnetSplValidationImplementationStatus {
|
||||
/// The scenario can be simulated and submitted through the application.
|
||||
Executable,
|
||||
/// The typed backend exists but the application execution path remains to be connected.
|
||||
BackendReady,
|
||||
/// The scenario requires externally prepared proof material before execution.
|
||||
ProofFixtureRequired,
|
||||
}
|
||||
|
||||
/// One independent Devnet validation scenario exposed to applications.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct DevnetSplValidationScenario {
|
||||
/// Stable identifier used by scripts, tests and the frontend.
|
||||
pub id: std::string::String,
|
||||
/// Operator-visible label.
|
||||
pub label: std::string::String,
|
||||
/// Scenario family.
|
||||
pub family: crate::DevnetSplValidationFamily,
|
||||
/// Stable executor operation code.
|
||||
pub operation_code: std::string::String,
|
||||
/// Current implementation status.
|
||||
pub implementation_status: crate::DevnetSplValidationImplementationStatus,
|
||||
/// Whether this scenario mutates on-chain state.
|
||||
pub destructive: bool,
|
||||
/// Whether explicit operator confirmation is mandatory before submission.
|
||||
pub operator_confirmation_required: bool,
|
||||
/// Whether cryptographic proof material is required.
|
||||
pub proof_required: bool,
|
||||
/// Ordered fixture variables required by the scenario.
|
||||
pub required_fixture_variables: std::vec::Vec<std::string::String>,
|
||||
}
|
||||
|
||||
/// Returns the complete ordered Devnet scenario inventory for milestone 0.4.6.
|
||||
pub fn devnet_spl_validation_scenarios() -> std::vec::Vec<crate::DevnetSplValidationScenario> {
|
||||
return vec![
|
||||
public_scenario("token_2022_mint_to_checked", "Token-2022 MintToChecked", kb_executor_spl_token_2022::MINT_TO_CHECKED_OPERATION, &["TOKEN_2022_MINT", "TOKEN_2022_SOURCE", "TOKEN_2022_AUTHORITY", "TOKEN_2022_DECIMALS", "TOKEN_2022_MINT_AMOUNT_RAW"]),
|
||||
public_scenario("token_2022_transfer_checked", "Token-2022 TransferChecked", kb_executor_spl_token_2022::TRANSFER_CHECKED_OPERATION, &["TOKEN_2022_SOURCE", "TOKEN_2022_MINT", "TOKEN_2022_DESTINATION", "TOKEN_2022_AUTHORITY", "TOKEN_2022_DECIMALS", "TOKEN_2022_TRANSFER_AMOUNT_RAW"]),
|
||||
public_scenario("token_2022_approve_checked", "Token-2022 ApproveChecked", kb_executor_spl_token_2022::APPROVE_CHECKED_OPERATION, &["TOKEN_2022_SOURCE", "TOKEN_2022_MINT", "TOKEN_2022_DELEGATE", "TOKEN_2022_AUTHORITY", "TOKEN_2022_DECIMALS", "TOKEN_2022_APPROVE_AMOUNT_RAW"]),
|
||||
public_scenario("token_2022_revoke", "Token-2022 Revoke", kb_executor_spl_token_2022::REVOKE_OPERATION, &["TOKEN_2022_SOURCE", "TOKEN_2022_AUTHORITY"]),
|
||||
public_scenario("token_2022_burn_checked", "Token-2022 BurnChecked", kb_executor_spl_token_2022::BURN_CHECKED_OPERATION, &["TOKEN_2022_SOURCE", "TOKEN_2022_MINT", "TOKEN_2022_AUTHORITY", "TOKEN_2022_DECIMALS", "TOKEN_2022_BURN_AMOUNT_RAW"]),
|
||||
public_scenario("token_2022_freeze_account", "Token-2022 FreezeAccount", kb_executor_spl_token_2022::FREEZE_ACCOUNT_OPERATION, &["TOKEN_2022_SOURCE", "TOKEN_2022_MINT", "TOKEN_2022_FREEZE_AUTHORITY"]),
|
||||
public_scenario("token_2022_thaw_account", "Token-2022 ThawAccount", kb_executor_spl_token_2022::THAW_ACCOUNT_OPERATION, &["TOKEN_2022_SOURCE", "TOKEN_2022_MINT", "TOKEN_2022_FREEZE_AUTHORITY"]),
|
||||
public_scenario("token_2022_close_destination", "Token-2022 CloseAccount destination", kb_executor_spl_token_2022::CLOSE_ACCOUNT_OPERATION, &["TOKEN_2022_DESTINATION", "KB_DEVNET_WALLET_ADDRESS", "TOKEN_2022_AUTHORITY"]),
|
||||
registry_scenario("elgamal_registry_create", "ElGamal Registry CreateRegistry", "spl_elgamal_registry.create_registry", &["ELGAMAL_REGISTRY_ADDRESS", "ELGAMAL_PUBKEY_BASE64", "PUBKEY_VALIDITY_PROOF_CONTEXT"]),
|
||||
registry_scenario("elgamal_registry_update", "ElGamal Registry UpdateRegistry", "spl_elgamal_registry.update_registry", &["ELGAMAL_REGISTRY_ADDRESS", "ELGAMAL_PUBKEY_BASE64", "PUBKEY_VALIDITY_PROOF_CONTEXT"]),
|
||||
confidential_scenario("token_2022_configure_confidential_account", "Token-2022 ConfigureAccountWithRegistry", kb_executor_spl_token_2022::CONFIGURE_CONFIDENTIAL_TRANSFER_ACCOUNT_WITH_REGISTRY_OPERATION, &["TOKEN_2022_SOURCE", "TOKEN_2022_MINT", "ELGAMAL_REGISTRY_ADDRESS", "PUBKEY_VALIDITY_PROOF_CONTEXT"]),
|
||||
];
|
||||
}
|
||||
|
||||
fn public_scenario(
|
||||
id: &str,
|
||||
label: &str,
|
||||
operation_code: &str,
|
||||
variables: &[&str],
|
||||
) -> crate::DevnetSplValidationScenario {
|
||||
return scenario(
|
||||
id,
|
||||
label,
|
||||
crate::DevnetSplValidationFamily::Token2022Public,
|
||||
operation_code,
|
||||
crate::DevnetSplValidationImplementationStatus::Executable,
|
||||
true,
|
||||
false,
|
||||
variables,
|
||||
);
|
||||
}
|
||||
|
||||
fn registry_scenario(
|
||||
id: &str,
|
||||
label: &str,
|
||||
operation_code: &str,
|
||||
variables: &[&str],
|
||||
) -> crate::DevnetSplValidationScenario {
|
||||
return scenario(
|
||||
id,
|
||||
label,
|
||||
crate::DevnetSplValidationFamily::ElGamalRegistry,
|
||||
operation_code,
|
||||
crate::DevnetSplValidationImplementationStatus::ProofFixtureRequired,
|
||||
true,
|
||||
true,
|
||||
variables,
|
||||
);
|
||||
}
|
||||
|
||||
fn confidential_scenario(
|
||||
id: &str,
|
||||
label: &str,
|
||||
operation_code: &str,
|
||||
variables: &[&str],
|
||||
) -> crate::DevnetSplValidationScenario {
|
||||
return scenario(
|
||||
id,
|
||||
label,
|
||||
crate::DevnetSplValidationFamily::Token2022Confidential,
|
||||
operation_code,
|
||||
crate::DevnetSplValidationImplementationStatus::ProofFixtureRequired,
|
||||
true,
|
||||
true,
|
||||
variables,
|
||||
);
|
||||
}
|
||||
|
||||
fn scenario(
|
||||
id: &str,
|
||||
label: &str,
|
||||
family: crate::DevnetSplValidationFamily,
|
||||
operation_code: &str,
|
||||
implementation_status: crate::DevnetSplValidationImplementationStatus,
|
||||
destructive: bool,
|
||||
proof_required: bool,
|
||||
variables: &[&str],
|
||||
) -> crate::DevnetSplValidationScenario {
|
||||
return crate::DevnetSplValidationScenario {
|
||||
id: id.to_string(),
|
||||
label: label.to_string(),
|
||||
family,
|
||||
operation_code: operation_code.to_string(),
|
||||
implementation_status,
|
||||
destructive,
|
||||
operator_confirmation_required: destructive,
|
||||
proof_required,
|
||||
required_fixture_variables: variables
|
||||
.iter()
|
||||
.map(|value| return (*value).to_string())
|
||||
.collect(),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn milestone_inventory_is_stable_unique_and_keeps_registry_separate() {
|
||||
let scenarios = crate::devnet_spl_validation_scenarios();
|
||||
assert_eq!(scenarios.len(), 11);
|
||||
let ids = scenarios
|
||||
.iter()
|
||||
.map(|scenario| return scenario.id.as_str())
|
||||
.collect::<std::collections::BTreeSet<&str>>();
|
||||
assert_eq!(ids.len(), scenarios.len());
|
||||
assert!(
|
||||
scenarios.iter().any(|scenario| return scenario.family
|
||||
== crate::DevnetSplValidationFamily::ElGamalRegistry)
|
||||
);
|
||||
assert!(
|
||||
scenarios
|
||||
.iter()
|
||||
.filter(|scenario| return scenario.family
|
||||
== crate::DevnetSplValidationFamily::Token2022Public)
|
||||
.all(|scenario| return !scenario.proof_required)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_mutating_scenario_requires_operator_confirmation() {
|
||||
assert!(crate::devnet_spl_validation_scenarios().iter().all(|scenario| {
|
||||
return !scenario.destructive || scenario.operator_confirmation_required;
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
// file: kb_pipeline/src/solana_token_2022_execution_orchestration.rs
|
||||
// version: 3
|
||||
|
||||
//! Final Token-2022 execution-readiness and stateful postcondition contracts.
|
||||
|
||||
/// Maximum number of distinct transaction signers accepted by one Token-2022 execution envelope.
|
||||
pub const MAX_TOKEN_2022_EXECUTION_SIGNERS: usize = 16;
|
||||
|
||||
/// Result of one stateful postcondition checked after a confirmed Token-2022 transaction.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Token2022ExecutionPostconditionStatus {
|
||||
/// The observed final state confirms the expected operation effect.
|
||||
Confirmed,
|
||||
/// The observed final state contradicts the expected operation effect.
|
||||
Contradicted,
|
||||
/// The operation has no supported stateful postcondition in the current contract.
|
||||
NotApplicable,
|
||||
}
|
||||
|
||||
/// One explicit stateful postcondition retained by the execution orchestrator.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct Token2022ExecutionPostcondition {
|
||||
/// Stable semantic role such as `mint`, `source`, `destination`, or `registry`.
|
||||
pub role: std::string::String,
|
||||
/// Canonical account whose final state was inspected.
|
||||
pub account: kb_model::Pubkey,
|
||||
/// Explicit result of the postcondition.
|
||||
pub status: crate::Token2022ExecutionPostconditionStatus,
|
||||
/// Bounded diagnostic explaining the result without retaining complete account data.
|
||||
pub diagnostic: std::string::String,
|
||||
}
|
||||
|
||||
/// Complete deterministic request checked before Token-2022 transaction signing.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Token2022ExecutionReadinessRequest {
|
||||
/// Exact prepared executor plan.
|
||||
pub plan: kb_execution_api::PreparedExecutionPlan,
|
||||
/// Exact hash of the compiled Solana message.
|
||||
pub message_hash: std::string::String,
|
||||
/// Hash retained by the simulation evidence.
|
||||
pub simulated_message_hash: std::string::String,
|
||||
/// Whether the exact compiled message was simulated.
|
||||
pub simulated: bool,
|
||||
/// Whether the exact simulation succeeded.
|
||||
pub simulation_succeeded: bool,
|
||||
/// Stateful Token-2022 account preflight report.
|
||||
pub stateful_preflight: crate::Token2022PreflightReport,
|
||||
/// Cryptographic proof context-state preflight report.
|
||||
pub cryptographic_preflight: crate::Token2022CryptographicPreflightReport,
|
||||
/// Ordered proof orchestration report.
|
||||
pub proof_orchestration: crate::Token2022ProofOrchestrationReport,
|
||||
/// Public keys actually available to sign the transaction.
|
||||
pub resolved_signers: std::vec::Vec<kb_model::Pubkey>,
|
||||
/// Whether submission was explicitly requested.
|
||||
pub submit: bool,
|
||||
/// Whether submission received explicit operator confirmation.
|
||||
pub operator_confirmed: bool,
|
||||
}
|
||||
|
||||
/// Deterministic execution-readiness report produced before signing.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct Token2022ExecutionReadinessReport {
|
||||
/// Stable operation code.
|
||||
pub operation_code: std::string::String,
|
||||
/// Exact compiled message hash bound to simulation.
|
||||
pub message_hash: std::string::String,
|
||||
/// Highest RPC context slot across stateful and cryptographic preflights.
|
||||
pub context_slot: u64,
|
||||
/// Ordered signer public keys required by the plan.
|
||||
pub required_signers: std::vec::Vec<kb_model::Pubkey>,
|
||||
/// Whether transaction signing and submission are authorized.
|
||||
pub send_authorized: bool,
|
||||
/// Ordered successful checks.
|
||||
pub checks: std::vec::Vec<std::string::String>,
|
||||
}
|
||||
|
||||
/// Validates the complete Token-2022 execution envelope before transaction signing.
|
||||
pub fn validate_token_2022_execution_readiness(
|
||||
request: &crate::Token2022ExecutionReadinessRequest,
|
||||
) -> kb_core::Result<crate::Token2022ExecutionReadinessReport> {
|
||||
if request.plan.operation_code.trim().is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Token-2022 execution plan operation_code must not be empty",
|
||||
));
|
||||
}
|
||||
if request.plan.operation_code != request.proof_orchestration.operation_code {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_execution_operation_mismatch",
|
||||
"Token-2022 plan and proof orchestration operation codes must match",
|
||||
));
|
||||
}
|
||||
if request.message_hash.trim().is_empty()
|
||||
|| request.simulated_message_hash.trim().is_empty()
|
||||
|| request.message_hash != request.simulated_message_hash
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_execution_simulation_message_mismatch",
|
||||
"Token-2022 simulation must be bound to the exact compiled message hash",
|
||||
));
|
||||
}
|
||||
if !request.simulated || !request.simulation_succeeded {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_execution_simulation_required",
|
||||
"Token-2022 execution requires one successful exact-message simulation",
|
||||
));
|
||||
}
|
||||
if !request.proof_orchestration.simulation_required {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_execution_proof_policy_mismatch",
|
||||
"Token-2022 proof orchestration must require simulation",
|
||||
));
|
||||
}
|
||||
if request.stateful_preflight.commitment != "confirmed"
|
||||
|| request.cryptographic_preflight.commitment != "confirmed"
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_execution_preflight_commitment_mismatch",
|
||||
"Token-2022 stateful and cryptographic preflights must use confirmed commitment",
|
||||
));
|
||||
}
|
||||
let required_signers = match validate_signers(
|
||||
request.plan.required_signers.as_slice(),
|
||||
request.resolved_signers.as_slice(),
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if request.submit && !request.operator_confirmed {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_execution_confirmation_required",
|
||||
"Token-2022 submission requires explicit operator confirmation",
|
||||
));
|
||||
}
|
||||
if request.submit && !request.proof_orchestration.send_authorized {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_execution_proof_send_not_authorized",
|
||||
"Token-2022 proof orchestration did not authorize submission",
|
||||
));
|
||||
}
|
||||
if request.submit && request.plan.policy.dry_run {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_execution_dry_run_blocks_submission",
|
||||
"Token-2022 plan remains dry-run and cannot be submitted",
|
||||
));
|
||||
}
|
||||
let context_slot = request
|
||||
.stateful_preflight
|
||||
.context_slot
|
||||
.max(request.cryptographic_preflight.context_slot);
|
||||
return std::result::Result::Ok(crate::Token2022ExecutionReadinessReport {
|
||||
operation_code: request.plan.operation_code.clone(),
|
||||
message_hash: request.message_hash.clone(),
|
||||
context_slot,
|
||||
required_signers,
|
||||
send_authorized: request.submit
|
||||
&& request.operator_confirmed
|
||||
&& request.proof_orchestration.send_authorized
|
||||
&& !request.plan.policy.dry_run,
|
||||
checks: vec![
|
||||
"operation_matches_proof_orchestration".to_string(),
|
||||
"simulation_bound_to_exact_message".to_string(),
|
||||
"confirmed_stateful_preflight".to_string(),
|
||||
"confirmed_cryptographic_preflight".to_string(),
|
||||
"all_required_signers_resolved".to_string(),
|
||||
"submission_policy_consistent".to_string(),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
/// Aggregates explicit postconditions without converting unsupported checks into success.
|
||||
pub fn summarize_token_2022_postconditions(
|
||||
postconditions: &[crate::Token2022ExecutionPostcondition],
|
||||
) -> crate::Token2022ExecutionPostconditionStatus {
|
||||
if postconditions.iter().any(|item| {
|
||||
return item.status == crate::Token2022ExecutionPostconditionStatus::Contradicted;
|
||||
}) {
|
||||
return crate::Token2022ExecutionPostconditionStatus::Contradicted;
|
||||
}
|
||||
if postconditions.iter().any(|item| {
|
||||
return item.status == crate::Token2022ExecutionPostconditionStatus::Confirmed;
|
||||
}) {
|
||||
return crate::Token2022ExecutionPostconditionStatus::Confirmed;
|
||||
}
|
||||
return crate::Token2022ExecutionPostconditionStatus::NotApplicable;
|
||||
}
|
||||
|
||||
fn validate_signers(
|
||||
required: &[kb_execution_api::RequiredSigner],
|
||||
resolved: &[kb_model::Pubkey],
|
||||
) -> kb_core::Result<std::vec::Vec<kb_model::Pubkey>> {
|
||||
if required.len() > crate::MAX_TOKEN_2022_EXECUTION_SIGNERS
|
||||
|| resolved.len() > crate::MAX_TOKEN_2022_EXECUTION_SIGNERS
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_execution_signer_limit_exceeded",
|
||||
format!(
|
||||
"Token-2022 execution accepts at most {} signers",
|
||||
crate::MAX_TOKEN_2022_EXECUTION_SIGNERS
|
||||
),
|
||||
));
|
||||
}
|
||||
let mut required_unique =
|
||||
std::collections::BTreeMap::<std::string::String, kb_model::Pubkey>::new();
|
||||
for signer in required {
|
||||
required_unique
|
||||
.entry(signer.pubkey.0.clone())
|
||||
.or_insert_with(|| return signer.pubkey.clone());
|
||||
}
|
||||
let resolved_set = resolved
|
||||
.iter()
|
||||
.map(|signer| return signer.0.clone())
|
||||
.collect::<std::collections::BTreeSet<std::string::String>>();
|
||||
for signer in required_unique.values() {
|
||||
if !resolved_set.contains(signer.0.as_str()) {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_execution_signer_unresolved",
|
||||
format!("Token-2022 required signer {} is unresolved", signer.0),
|
||||
));
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(required_unique.into_values().collect());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
fn pubkey(byte: u8) -> kb_model::Pubkey {
|
||||
return kb_model::Pubkey(bs58::encode([byte; 32]).into_string());
|
||||
}
|
||||
|
||||
fn request() -> crate::Token2022ExecutionReadinessRequest {
|
||||
let signer = pubkey(1);
|
||||
let policy = kb_execution_api::ExecutionPolicy {
|
||||
dry_run: false,
|
||||
..std::default::Default::default()
|
||||
};
|
||||
let plan = kb_execution_api::PreparedExecutionPlan {
|
||||
executor_name: "kb_executor_spl_token_2022".to_string(),
|
||||
executor_version: "0.4.6".to_string(),
|
||||
intent_id: "intent".to_string(),
|
||||
operation_code: "spl_token_2022.confidential_transfer".to_string(),
|
||||
fee_payer: signer.clone(),
|
||||
instructions: vec![],
|
||||
required_signers: vec![kb_execution_api::RequiredSigner {
|
||||
pubkey: signer.clone(),
|
||||
role: "authority".to_string(),
|
||||
}],
|
||||
policy,
|
||||
requested_spend_lamports: 0,
|
||||
requested_compute_unit_price_micro_lamports: std::option::Option::None,
|
||||
};
|
||||
return crate::Token2022ExecutionReadinessRequest {
|
||||
plan,
|
||||
message_hash: "message-hash".to_string(),
|
||||
simulated_message_hash: "message-hash".to_string(),
|
||||
simulated: true,
|
||||
simulation_succeeded: true,
|
||||
stateful_preflight: crate::Token2022PreflightReport {
|
||||
commitment: "confirmed".to_string(),
|
||||
context_slot: 100,
|
||||
requested_data_bytes: 0,
|
||||
accounts: vec![],
|
||||
elgamal_registry_validated: false,
|
||||
},
|
||||
cryptographic_preflight: crate::Token2022CryptographicPreflightReport {
|
||||
commitment: "confirmed".to_string(),
|
||||
context_slot: 101,
|
||||
proof_contexts: vec![],
|
||||
},
|
||||
proof_orchestration: crate::Token2022ProofOrchestrationReport {
|
||||
operation_code: "spl_token_2022.confidential_transfer".to_string(),
|
||||
instructions_sysvar_required: true,
|
||||
inline_offsets: vec![1],
|
||||
context_requirements: vec![],
|
||||
simulation_required: true,
|
||||
send_authorized: true,
|
||||
checks: vec![],
|
||||
},
|
||||
resolved_signers: vec![signer],
|
||||
submit: true,
|
||||
operator_confirmed: true,
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_message_preflights_and_signers_authorize_submission() {
|
||||
let report = crate::validate_token_2022_execution_readiness(&request())
|
||||
.expect("complete Token-2022 readiness must succeed");
|
||||
assert!(report.send_authorized);
|
||||
assert_eq!(report.context_slot, 101);
|
||||
assert_eq!(report.required_signers.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_mismatch_missing_signer_and_dry_run_fail_closed() {
|
||||
let mut mismatched = request();
|
||||
mismatched.simulated_message_hash = "other".to_string();
|
||||
assert!(crate::validate_token_2022_execution_readiness(&mismatched).is_err());
|
||||
let mut missing = request();
|
||||
missing.resolved_signers.clear();
|
||||
assert!(crate::validate_token_2022_execution_readiness(&missing).is_err());
|
||||
let mut dry_run = request();
|
||||
dry_run.plan.policy.dry_run = true;
|
||||
assert!(crate::validate_token_2022_execution_readiness(&dry_run).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn postcondition_summary_preserves_contradicted_and_not_applicable() {
|
||||
let account = pubkey(2);
|
||||
let not_applicable = crate::Token2022ExecutionPostcondition {
|
||||
role: "mint".to_string(),
|
||||
account: account.clone(),
|
||||
status: crate::Token2022ExecutionPostconditionStatus::NotApplicable,
|
||||
diagnostic: "no supported final-state assertion".to_string(),
|
||||
};
|
||||
assert_eq!(
|
||||
crate::summarize_token_2022_postconditions(&[not_applicable]),
|
||||
crate::Token2022ExecutionPostconditionStatus::NotApplicable
|
||||
);
|
||||
let contradicted = crate::Token2022ExecutionPostcondition {
|
||||
role: "source".to_string(),
|
||||
account,
|
||||
status: crate::Token2022ExecutionPostconditionStatus::Contradicted,
|
||||
diagnostic: "final extension inventory contradicts the expected effect".to_string(),
|
||||
};
|
||||
assert_eq!(
|
||||
crate::summarize_token_2022_postconditions(&[contradicted]),
|
||||
crate::Token2022ExecutionPostconditionStatus::Contradicted
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,445 @@
|
||||
// file: kb_pipeline/src/solana_token_2022_preflight.rs
|
||||
// version: 1
|
||||
|
||||
//! Bounded Token-2022 stateful preflight orchestration.
|
||||
|
||||
/// Maximum distinct Token-2022 accounts accepted by one preflight inspection.
|
||||
pub const MAX_TOKEN_2022_PREFLIGHT_ACCOUNTS: usize = 16;
|
||||
/// Maximum aggregate account-data budget accepted by one preflight inspection.
|
||||
pub const MAX_TOKEN_2022_PREFLIGHT_TOTAL_BYTES: usize = 262_144;
|
||||
|
||||
/// One exact Token-2022 account requirement for a stateful preflight.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Token2022PreflightRequirement {
|
||||
/// Stable semantic role used in diagnostics.
|
||||
pub role: std::string::String,
|
||||
/// Canonical account address.
|
||||
pub account: kb_model::Pubkey,
|
||||
/// Expected Token-2022 state category.
|
||||
pub kind: kb_decoder_spl_token_2022::state::Token2022StateKind,
|
||||
/// Maximum complete account bytes accepted for this requirement.
|
||||
pub max_data_bytes: usize,
|
||||
/// Optional expected mint for a Token Account.
|
||||
pub expected_mint: std::option::Option<kb_model::Pubkey>,
|
||||
/// Optional expected owner for a Token Account.
|
||||
pub expected_owner: std::option::Option<kb_model::Pubkey>,
|
||||
/// Optional exact decimals expected for a Mint.
|
||||
pub expected_decimals: std::option::Option<u8>,
|
||||
/// Published extension names required on the account.
|
||||
pub required_extensions: std::vec::Vec<std::string::String>,
|
||||
/// Optional external identities needed by cross-account extensions.
|
||||
pub context: crate::Token2022StatefulContext,
|
||||
}
|
||||
|
||||
/// One bounded Token-2022 preflight request.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Token2022PreflightRequest {
|
||||
/// Endpoint role used by all RPC reads.
|
||||
pub query_role: std::string::String,
|
||||
/// Optional minimum context slot shared by all reads.
|
||||
pub min_context_slot: std::option::Option<u64>,
|
||||
/// Maximum distinct accounts accepted after deduplication.
|
||||
pub max_accounts: usize,
|
||||
/// Maximum aggregate requested account bytes.
|
||||
pub max_total_data_bytes: usize,
|
||||
/// Ordered account requirements.
|
||||
pub requirements: std::vec::Vec<Token2022PreflightRequirement>,
|
||||
/// Optional ElGamal registry read required by the operation.
|
||||
pub elgamal_registry: std::option::Option<crate::ElGamalRegistryStatefulReadRequest>,
|
||||
}
|
||||
|
||||
/// One validated account result in a Token-2022 preflight report.
|
||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct Token2022PreflightAccountReport {
|
||||
/// Stable semantic role.
|
||||
pub role: std::string::String,
|
||||
/// Canonical account address.
|
||||
pub account: kb_model::Pubkey,
|
||||
/// State category observed after parsing.
|
||||
pub state_kind: std::string::String,
|
||||
/// Context slot returned by the endpoint.
|
||||
pub context_slot: u64,
|
||||
/// Ordered extension names observed on the account.
|
||||
pub extension_names: std::vec::Vec<std::string::String>,
|
||||
/// Ordered successful semantic checks.
|
||||
pub checks: std::vec::Vec<std::string::String>,
|
||||
}
|
||||
|
||||
/// Complete bounded Token-2022 stateful preflight report.
|
||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct Token2022PreflightReport {
|
||||
/// Commitment used for all account reads.
|
||||
pub commitment: std::string::String,
|
||||
/// Highest context slot observed across all reads.
|
||||
pub context_slot: u64,
|
||||
/// Aggregate requested account-data budget.
|
||||
pub requested_data_bytes: usize,
|
||||
/// Ordered distinct Token-2022 account reports.
|
||||
pub accounts: std::vec::Vec<Token2022PreflightAccountReport>,
|
||||
/// Whether an ElGamal registry was required and validated.
|
||||
pub elgamal_registry_validated: bool,
|
||||
}
|
||||
|
||||
/// Inspects all bounded Token-2022 state required before simulation.
|
||||
pub async fn inspect_token_2022_preflight(
|
||||
pool: &kb_rpc::HttpEndpointPool,
|
||||
request: &crate::Token2022PreflightRequest,
|
||||
) -> kb_core::Result<crate::Token2022PreflightReport> {
|
||||
let requirements = match validate_and_deduplicate_requirements(request) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let requested_data_bytes = requirements
|
||||
.iter()
|
||||
.fold(0usize, |total, requirement| {
|
||||
return total.saturating_add(requirement.max_data_bytes);
|
||||
})
|
||||
.saturating_add(if request.elgamal_registry.is_some() {
|
||||
crate::ELGAMAL_REGISTRY_STATEFUL_ACCOUNT_BYTES
|
||||
} else {
|
||||
0
|
||||
});
|
||||
let mut context_slot = request.min_context_slot.unwrap_or(0);
|
||||
let mut accounts = std::vec::Vec::with_capacity(requirements.len());
|
||||
for requirement in requirements {
|
||||
let read_request = crate::Token2022StatefulReadRequest {
|
||||
query_role: request.query_role.clone(),
|
||||
account: requirement.account.clone(),
|
||||
kind: requirement.kind,
|
||||
min_context_slot: request.min_context_slot,
|
||||
max_data_bytes: requirement.max_data_bytes,
|
||||
context: requirement.context.clone(),
|
||||
};
|
||||
let read_result = match crate::read_token_2022_stateful_snapshot(pool, &read_request).await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let account_report = match validate_snapshot_requirement(&requirement, &read_result) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
context_slot = context_slot.max(account_report.context_slot);
|
||||
accounts.push(account_report);
|
||||
}
|
||||
let mut elgamal_registry_validated = false;
|
||||
if let std::option::Option::Some(registry_request) = request.elgamal_registry.as_ref() {
|
||||
if registry_request.query_role != request.query_role {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Token-2022 preflight ElGamal registry query role must match the shared query role",
|
||||
));
|
||||
}
|
||||
let registry_result =
|
||||
match crate::read_elgamal_registry_stateful_snapshot(pool, registry_request).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
context_slot = context_slot.max(registry_result.context_slot);
|
||||
elgamal_registry_validated = true;
|
||||
}
|
||||
return std::result::Result::Ok(crate::Token2022PreflightReport {
|
||||
commitment: "confirmed".to_string(),
|
||||
context_slot,
|
||||
requested_data_bytes,
|
||||
accounts,
|
||||
elgamal_registry_validated,
|
||||
});
|
||||
}
|
||||
|
||||
fn validate_and_deduplicate_requirements(
|
||||
request: &crate::Token2022PreflightRequest,
|
||||
) -> kb_core::Result<std::vec::Vec<crate::Token2022PreflightRequirement>> {
|
||||
if request.query_role.trim().is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Token-2022 preflight query_role must not be empty",
|
||||
));
|
||||
}
|
||||
if request.max_accounts == 0 || request.max_accounts > crate::MAX_TOKEN_2022_PREFLIGHT_ACCOUNTS
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::config(format!(
|
||||
"Token-2022 preflight max_accounts must be between 1 and {}",
|
||||
crate::MAX_TOKEN_2022_PREFLIGHT_ACCOUNTS
|
||||
)));
|
||||
}
|
||||
if request.max_total_data_bytes == 0
|
||||
|| request.max_total_data_bytes > crate::MAX_TOKEN_2022_PREFLIGHT_TOTAL_BYTES
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::config(format!(
|
||||
"Token-2022 preflight max_total_data_bytes must be between 1 and {}",
|
||||
crate::MAX_TOKEN_2022_PREFLIGHT_TOTAL_BYTES
|
||||
)));
|
||||
}
|
||||
if request.requirements.is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Token-2022 preflight requires at least one account",
|
||||
));
|
||||
}
|
||||
let mut indexes = std::collections::BTreeMap::<std::string::String, usize>::new();
|
||||
let mut unique = std::vec::Vec::<crate::Token2022PreflightRequirement>::new();
|
||||
for requirement in &request.requirements {
|
||||
if requirement.role.trim().is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Token-2022 preflight requirement role must not be empty",
|
||||
));
|
||||
}
|
||||
if requirement.max_data_bytes == 0
|
||||
|| requirement.max_data_bytes > crate::MAX_TOKEN_2022_STATEFUL_ACCOUNT_BYTES
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::config(format!(
|
||||
"Token-2022 preflight account {} has invalid max_data_bytes {}",
|
||||
requirement.account.0, requirement.max_data_bytes
|
||||
)));
|
||||
}
|
||||
if let std::option::Option::Some(index) = indexes.get(requirement.account.0.as_str()) {
|
||||
if &unique[*index] != requirement {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_preflight_conflicting_duplicate",
|
||||
format!(
|
||||
"Token-2022 preflight account {} has conflicting requirements",
|
||||
requirement.account.0
|
||||
),
|
||||
));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
indexes.insert(requirement.account.0.clone(), unique.len());
|
||||
unique.push(requirement.clone());
|
||||
}
|
||||
let total_accounts =
|
||||
unique
|
||||
.len()
|
||||
.saturating_add(if request.elgamal_registry.is_some() { 1 } else { 0 });
|
||||
if total_accounts > request.max_accounts {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_preflight_account_limit_exceeded",
|
||||
format!(
|
||||
"Token-2022 preflight requires {total_accounts} distinct accounts above limit {}",
|
||||
request.max_accounts
|
||||
),
|
||||
));
|
||||
}
|
||||
let requested_data_bytes = unique
|
||||
.iter()
|
||||
.fold(0usize, |total, requirement| {
|
||||
return total.saturating_add(requirement.max_data_bytes);
|
||||
})
|
||||
.saturating_add(if request.elgamal_registry.is_some() {
|
||||
crate::ELGAMAL_REGISTRY_STATEFUL_ACCOUNT_BYTES
|
||||
} else {
|
||||
0
|
||||
});
|
||||
if requested_data_bytes > request.max_total_data_bytes {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_preflight_data_budget_exceeded",
|
||||
format!(
|
||||
"Token-2022 preflight requests {requested_data_bytes} bytes above aggregate limit {}",
|
||||
request.max_total_data_bytes
|
||||
),
|
||||
));
|
||||
}
|
||||
return std::result::Result::Ok(unique);
|
||||
}
|
||||
|
||||
fn validate_snapshot_requirement(
|
||||
requirement: &crate::Token2022PreflightRequirement,
|
||||
result: &crate::Token2022StatefulReadResult,
|
||||
) -> kb_core::Result<crate::Token2022PreflightAccountReport> {
|
||||
let token_output = result
|
||||
.snapshot
|
||||
.outputs
|
||||
.iter()
|
||||
.find(|output| return output.payload_json["domain"] == "spl_token_2022_account_state");
|
||||
let token_output = match token_output {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(
|
||||
"Token-2022 preflight snapshot is missing its token account owner projection",
|
||||
));
|
||||
},
|
||||
};
|
||||
let base_fields = &token_output.payload_json["baseFields"];
|
||||
let mut checks = std::vec![
|
||||
"owner_program_id".to_string(),
|
||||
"complete_account_data".to_string(),
|
||||
"state_kind".to_string()
|
||||
];
|
||||
if let std::option::Option::Some(expected_mint) = requirement.expected_mint.as_ref() {
|
||||
if base_fields["mint"].as_str() != std::option::Option::Some(expected_mint.0.as_str()) {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_preflight_mint_mismatch",
|
||||
format!(
|
||||
"Token-2022 account {} mint does not match expected {}",
|
||||
requirement.account.0, expected_mint.0
|
||||
),
|
||||
));
|
||||
}
|
||||
checks.push("mint_identity".to_string());
|
||||
}
|
||||
if let std::option::Option::Some(expected_owner) = requirement.expected_owner.as_ref() {
|
||||
if base_fields["owner"].as_str() != std::option::Option::Some(expected_owner.0.as_str()) {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_preflight_owner_mismatch",
|
||||
format!(
|
||||
"Token-2022 account {} owner does not match expected {}",
|
||||
requirement.account.0, expected_owner.0
|
||||
),
|
||||
));
|
||||
}
|
||||
checks.push("token_account_owner".to_string());
|
||||
}
|
||||
if let std::option::Option::Some(expected_decimals) = requirement.expected_decimals {
|
||||
if base_fields["decimals"].as_u64() != std::option::Option::Some(expected_decimals as u64) {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_preflight_decimals_mismatch",
|
||||
format!(
|
||||
"Token-2022 mint {} decimals do not match expected {expected_decimals}",
|
||||
requirement.account.0
|
||||
),
|
||||
));
|
||||
}
|
||||
checks.push("mint_decimals".to_string());
|
||||
}
|
||||
for required_extension in &requirement.required_extensions {
|
||||
if !result
|
||||
.snapshot
|
||||
.extension_names
|
||||
.iter()
|
||||
.any(|value| return value == required_extension)
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_preflight_extension_missing",
|
||||
format!(
|
||||
"Token-2022 account {} is missing required extension {required_extension}",
|
||||
requirement.account.0
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
if !requirement.required_extensions.is_empty() {
|
||||
checks.push("required_extensions".to_string());
|
||||
}
|
||||
return std::result::Result::Ok(crate::Token2022PreflightAccountReport {
|
||||
role: requirement.role.clone(),
|
||||
account: requirement.account.clone(),
|
||||
state_kind: result.snapshot.state_kind.clone(),
|
||||
context_slot: result.context_slot,
|
||||
extension_names: result.snapshot.extension_names.clone(),
|
||||
checks,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
fn pubkey(byte: u8) -> kb_model::Pubkey {
|
||||
return kb_model::Pubkey(bs58::encode([byte; 32]).into_string());
|
||||
}
|
||||
|
||||
fn requirement(account: kb_model::Pubkey) -> crate::Token2022PreflightRequirement {
|
||||
return crate::Token2022PreflightRequirement {
|
||||
role: "source".to_string(),
|
||||
account,
|
||||
kind: kb_decoder_spl_token_2022::state::Token2022StateKind::Account,
|
||||
max_data_bytes: 512,
|
||||
expected_mint: std::option::Option::None,
|
||||
expected_owner: std::option::Option::None,
|
||||
expected_decimals: std::option::Option::None,
|
||||
required_extensions: std::vec::Vec::new(),
|
||||
context: crate::Token2022StatefulContext::default(),
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_duplicate_requirements_are_deduplicated_and_bounded() {
|
||||
let item = requirement(pubkey(1));
|
||||
let request = crate::Token2022PreflightRequest {
|
||||
query_role: "query".to_string(),
|
||||
min_context_slot: std::option::Option::Some(7),
|
||||
max_accounts: 1,
|
||||
max_total_data_bytes: 512,
|
||||
requirements: std::vec![item.clone(), item],
|
||||
elgamal_registry: std::option::Option::None,
|
||||
};
|
||||
let result =
|
||||
crate::solana_token_2022_preflight::validate_and_deduplicate_requirements(&request);
|
||||
assert_eq!(result.as_ref().map(std::vec::Vec::len), std::result::Result::Ok(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conflicting_duplicates_and_aggregate_budget_fail_closed() {
|
||||
let first = requirement(pubkey(2));
|
||||
let mut conflicting = first.clone();
|
||||
conflicting.role = "destination".to_string();
|
||||
let conflict_request = crate::Token2022PreflightRequest {
|
||||
query_role: "query".to_string(),
|
||||
min_context_slot: std::option::Option::None,
|
||||
max_accounts: 2,
|
||||
max_total_data_bytes: 1024,
|
||||
requirements: std::vec![first, conflicting],
|
||||
elgamal_registry: std::option::Option::None,
|
||||
};
|
||||
assert!(
|
||||
crate::solana_token_2022_preflight::validate_and_deduplicate_requirements(
|
||||
&conflict_request
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
let budget_request = crate::Token2022PreflightRequest {
|
||||
query_role: "query".to_string(),
|
||||
min_context_slot: std::option::Option::None,
|
||||
max_accounts: 2,
|
||||
max_total_data_bytes: 700,
|
||||
requirements: std::vec![requirement(pubkey(3)), requirement(pubkey(4))],
|
||||
elgamal_registry: std::option::Option::None,
|
||||
};
|
||||
assert!(
|
||||
crate::solana_token_2022_preflight::validate_and_deduplicate_requirements(
|
||||
&budget_request
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requirement_validation_checks_mint_owner_decimals_and_extensions() {
|
||||
let account = pubkey(5);
|
||||
let mint = pubkey(6);
|
||||
let owner = pubkey(7);
|
||||
let requirement = crate::Token2022PreflightRequirement {
|
||||
role: "source".to_string(),
|
||||
account: account.clone(),
|
||||
kind: kb_decoder_spl_token_2022::state::Token2022StateKind::Account,
|
||||
max_data_bytes: 512,
|
||||
expected_mint: std::option::Option::Some(mint.clone()),
|
||||
expected_owner: std::option::Option::Some(owner.clone()),
|
||||
expected_decimals: std::option::Option::None,
|
||||
required_extensions: std::vec!["memo_transfer".to_string()],
|
||||
context: crate::Token2022StatefulContext::default(),
|
||||
};
|
||||
let result = crate::Token2022StatefulReadResult {
|
||||
commitment: "confirmed".to_string(),
|
||||
context_slot: 42,
|
||||
snapshot: crate::Token2022StatefulSnapshotBundle {
|
||||
account_key: account.0.clone(),
|
||||
slot: 42,
|
||||
state_kind: "account".to_string(),
|
||||
extension_names: std::vec!["memo_transfer".to_string()],
|
||||
outputs: std::vec![kb_materializer_api::MaterializedOutput {
|
||||
output_key: "state".to_string(),
|
||||
family: kb_model::MaterializedEventFamily::TokenAccount,
|
||||
payload_json: serde_json::json!({
|
||||
"domain":"spl_token_2022_account_state",
|
||||
"baseFields":{"mint":mint.0,"owner":owner.0}
|
||||
}),
|
||||
}],
|
||||
},
|
||||
};
|
||||
let report = crate::solana_token_2022_preflight::validate_snapshot_requirement(
|
||||
&requirement,
|
||||
&result,
|
||||
);
|
||||
assert_eq!(
|
||||
report.as_ref().map(|value| return value.checks.len()),
|
||||
std::result::Result::Ok(6)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
// file: kb_pipeline/src/solana_token_2022_proof_orchestration.rs
|
||||
// version: 2
|
||||
|
||||
//! Deterministic orchestration contract for mixed Token-2022 proof locations.
|
||||
|
||||
/// Maximum proof references accepted by one confidential Token-2022 operation.
|
||||
pub const MAX_TOKEN_2022_OPERATION_PROOFS: usize = 5;
|
||||
|
||||
/// One bounded proof-orchestration request prepared before transaction assembly.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Token2022ProofOrchestrationRequest {
|
||||
/// Stable executor operation code.
|
||||
pub operation_code: std::string::String,
|
||||
/// Exact ordered proof references required by the operation.
|
||||
pub proofs: std::vec::Vec<kb_executor_spl_token_2022::SplTokenConfidentialProofReference>,
|
||||
/// Optional authority expected on every context-state account.
|
||||
pub expected_context_authority: std::option::Option<kb_model::Pubkey>,
|
||||
/// Maximum compute-unit limit accepted for the future transaction.
|
||||
pub compute_unit_limit: u32,
|
||||
/// Maximum total fee accepted for the future transaction.
|
||||
pub max_fee_lamports: u64,
|
||||
/// Whether a future submission was explicitly requested.
|
||||
pub submit: bool,
|
||||
/// Whether the operator explicitly confirmed the future submission.
|
||||
pub operator_confirmed: bool,
|
||||
}
|
||||
|
||||
/// One deterministic mixed-proof orchestration result.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Token2022ProofOrchestrationReport {
|
||||
/// Stable executor operation code.
|
||||
pub operation_code: std::string::String,
|
||||
/// Whether the instructions sysvar must be present.
|
||||
pub instructions_sysvar_required: bool,
|
||||
/// Ordered non-zero inline offsets.
|
||||
pub inline_offsets: std::vec::Vec<i8>,
|
||||
/// Ordered validated context-state requirements.
|
||||
pub context_requirements: std::vec::Vec<crate::Token2022ProofContextRequirement>,
|
||||
/// Simulation is always mandatory.
|
||||
pub simulation_required: bool,
|
||||
/// Whether the future send path is authorized by request-local policy.
|
||||
pub send_authorized: bool,
|
||||
/// Ordered successful checks.
|
||||
pub checks: std::vec::Vec<std::string::String>,
|
||||
}
|
||||
|
||||
/// Validates mixed inline/context-state proof orchestration before transaction assembly.
|
||||
pub fn orchestrate_token_2022_proofs(
|
||||
request: &crate::Token2022ProofOrchestrationRequest,
|
||||
cryptographic_preflight: &crate::Token2022CryptographicPreflightReport,
|
||||
) -> kb_core::Result<crate::Token2022ProofOrchestrationReport> {
|
||||
if request.operation_code.trim().is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Token-2022 proof orchestration operation_code must not be empty",
|
||||
));
|
||||
}
|
||||
if request.proofs.len() > crate::MAX_TOKEN_2022_OPERATION_PROOFS {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_operation_proof_limit_exceeded",
|
||||
format!(
|
||||
"Token-2022 proof orchestration accepts at most {} proofs",
|
||||
crate::MAX_TOKEN_2022_OPERATION_PROOFS
|
||||
),
|
||||
));
|
||||
}
|
||||
if request.compute_unit_limit == 0 || request.max_fee_lamports == 0 {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Token-2022 proof orchestration requires non-zero compute and fee ceilings",
|
||||
));
|
||||
}
|
||||
if request.submit && !request.operator_confirmed {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_submission_confirmation_required",
|
||||
"Token-2022 submission requires explicit operator confirmation",
|
||||
));
|
||||
}
|
||||
let mut kinds = std::collections::BTreeSet::<std::string::String>::new();
|
||||
let mut offsets = std::collections::BTreeSet::<i8>::new();
|
||||
let mut context_accounts = std::collections::BTreeSet::<std::string::String>::new();
|
||||
let mut inline_offsets = std::vec::Vec::<i8>::new();
|
||||
let mut context_requirements = std::vec::Vec::<crate::Token2022ProofContextRequirement>::new();
|
||||
for proof in &request.proofs {
|
||||
let kind_code = proof_kind_code(proof.kind).to_string();
|
||||
if !kinds.insert(kind_code.clone()) {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_duplicate_proof_kind",
|
||||
format!(
|
||||
"Token-2022 operation {} contains duplicate proof kind {kind_code}",
|
||||
request.operation_code
|
||||
),
|
||||
));
|
||||
}
|
||||
if let std::result::Result::Err(error) = proof.location.validate() {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(error));
|
||||
}
|
||||
match &proof.location {
|
||||
kb_executor_spl_token_2022::SplTokenConfidentialProofLocation::InstructionOffset { offset } => {
|
||||
if !offsets.insert(*offset) {
|
||||
return std::result::Result::Err(kb_core::Error::new("token_2022_duplicate_inline_proof_offset", format!("Token-2022 operation {} reuses inline proof offset {offset}", request.operation_code)));
|
||||
}
|
||||
inline_offsets.push(*offset);
|
||||
},
|
||||
kb_executor_spl_token_2022::SplTokenConfidentialProofLocation::ContextStateAccount { account } => {
|
||||
if !context_accounts.insert(account.0.clone()) {
|
||||
return std::result::Result::Err(kb_core::Error::new("token_2022_duplicate_context_state_account", format!("Token-2022 operation {} reuses context-state account {}", request.operation_code, account.0)));
|
||||
}
|
||||
context_requirements.push(crate::Token2022ProofContextRequirement { role: kind_code, account: account.clone(), proof_type: proof_kind_to_zk_type(proof.kind), expected_authority: request.expected_context_authority.clone() });
|
||||
},
|
||||
}
|
||||
}
|
||||
if let std::result::Result::Err(error) =
|
||||
validate_context_reports(context_requirements.as_slice(), cryptographic_preflight)
|
||||
{
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
return std::result::Result::Ok(crate::Token2022ProofOrchestrationReport {
|
||||
operation_code: request.operation_code.clone(),
|
||||
instructions_sysvar_required: !inline_offsets.is_empty(),
|
||||
inline_offsets,
|
||||
context_requirements,
|
||||
simulation_required: true,
|
||||
send_authorized: request.submit && request.operator_confirmed,
|
||||
checks: vec![
|
||||
"ordered_unique_proof_kinds".to_string(),
|
||||
"non_zero_unique_inline_offsets".to_string(),
|
||||
"unique_context_state_accounts".to_string(),
|
||||
"context_preflight_matches_operation".to_string(),
|
||||
"simulation_required".to_string(),
|
||||
"bounded_compute_and_fee".to_string(),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
fn validate_context_reports(
|
||||
requirements: &[crate::Token2022ProofContextRequirement],
|
||||
report: &crate::Token2022CryptographicPreflightReport,
|
||||
) -> kb_core::Result<()> {
|
||||
if requirements.len() != report.proof_contexts.len() {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_context_preflight_count_mismatch",
|
||||
format!(
|
||||
"Token-2022 operation requires {} context states, preflight contains {}",
|
||||
requirements.len(),
|
||||
report.proof_contexts.len()
|
||||
),
|
||||
));
|
||||
}
|
||||
for (requirement, observed) in requirements.iter().zip(report.proof_contexts.iter()) {
|
||||
if requirement.account != observed.account
|
||||
|| requirement.proof_type.discriminator() != observed.proof_discriminator
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_context_preflight_mismatch",
|
||||
format!(
|
||||
"Token-2022 proof context {} does not match the ordered operation requirement",
|
||||
observed.account.0
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn proof_kind_code(
|
||||
kind: kb_executor_spl_token_2022::SplTokenConfidentialProofKind,
|
||||
) -> &'static str {
|
||||
return match kind {
|
||||
kb_executor_spl_token_2022::SplTokenConfidentialProofKind::PubkeyValidity => "pubkey_validity",
|
||||
kb_executor_spl_token_2022::SplTokenConfidentialProofKind::ZeroCiphertext => "zero_ciphertext",
|
||||
kb_executor_spl_token_2022::SplTokenConfidentialProofKind::CiphertextCommitmentEquality => "ciphertext_commitment_equality",
|
||||
kb_executor_spl_token_2022::SplTokenConfidentialProofKind::CiphertextCiphertextEquality => "ciphertext_ciphertext_equality",
|
||||
kb_executor_spl_token_2022::SplTokenConfidentialProofKind::BatchedGroupedCiphertext3HandlesValidity => "batched_grouped_ciphertext_3_handles_validity",
|
||||
kb_executor_spl_token_2022::SplTokenConfidentialProofKind::BatchedGroupedCiphertext2HandlesValidity => "batched_grouped_ciphertext_2_handles_validity",
|
||||
kb_executor_spl_token_2022::SplTokenConfidentialProofKind::PercentageWithFee => "percentage_with_fee",
|
||||
kb_executor_spl_token_2022::SplTokenConfidentialProofKind::BatchedRangeProofU64 => "batched_range_proof_u64",
|
||||
kb_executor_spl_token_2022::SplTokenConfidentialProofKind::BatchedRangeProofU128 => "batched_range_proof_u128",
|
||||
kb_executor_spl_token_2022::SplTokenConfidentialProofKind::BatchedRangeProofU256 => "batched_range_proof_u256",
|
||||
};
|
||||
}
|
||||
|
||||
fn proof_kind_to_zk_type(
|
||||
kind: kb_executor_spl_token_2022::SplTokenConfidentialProofKind,
|
||||
) -> kb_executor_solana_core::ZkElGamalProofType {
|
||||
return match kind {
|
||||
kb_executor_spl_token_2022::SplTokenConfidentialProofKind::PubkeyValidity => kb_executor_solana_core::ZkElGamalProofType::PubkeyValidity,
|
||||
kb_executor_spl_token_2022::SplTokenConfidentialProofKind::ZeroCiphertext => kb_executor_solana_core::ZkElGamalProofType::ZeroCiphertext,
|
||||
kb_executor_spl_token_2022::SplTokenConfidentialProofKind::CiphertextCommitmentEquality => kb_executor_solana_core::ZkElGamalProofType::CiphertextCommitmentEquality,
|
||||
kb_executor_spl_token_2022::SplTokenConfidentialProofKind::CiphertextCiphertextEquality => kb_executor_solana_core::ZkElGamalProofType::CiphertextCiphertextEquality,
|
||||
kb_executor_spl_token_2022::SplTokenConfidentialProofKind::BatchedGroupedCiphertext3HandlesValidity => kb_executor_solana_core::ZkElGamalProofType::BatchedGroupedCiphertext3HandlesValidity,
|
||||
kb_executor_spl_token_2022::SplTokenConfidentialProofKind::BatchedGroupedCiphertext2HandlesValidity => kb_executor_solana_core::ZkElGamalProofType::BatchedGroupedCiphertext2HandlesValidity,
|
||||
kb_executor_spl_token_2022::SplTokenConfidentialProofKind::PercentageWithFee => kb_executor_solana_core::ZkElGamalProofType::PercentageWithCap,
|
||||
kb_executor_spl_token_2022::SplTokenConfidentialProofKind::BatchedRangeProofU64 => kb_executor_solana_core::ZkElGamalProofType::BatchedRangeProofU64,
|
||||
kb_executor_spl_token_2022::SplTokenConfidentialProofKind::BatchedRangeProofU128 => kb_executor_solana_core::ZkElGamalProofType::BatchedRangeProofU128,
|
||||
kb_executor_spl_token_2022::SplTokenConfidentialProofKind::BatchedRangeProofU256 => kb_executor_solana_core::ZkElGamalProofType::BatchedRangeProofU256,
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
fn pubkey(byte: u8) -> kb_model::Pubkey {
|
||||
return kb_model::Pubkey(bs58::encode([byte; 32]).into_string());
|
||||
}
|
||||
|
||||
fn context_report(
|
||||
account: kb_model::Pubkey,
|
||||
proof_type: kb_executor_solana_core::ZkElGamalProofType,
|
||||
) -> crate::Token2022ProofContextReport {
|
||||
return crate::Token2022ProofContextReport {
|
||||
role: "proof".to_string(),
|
||||
account,
|
||||
proof_discriminator: proof_type.discriminator(),
|
||||
context_state_bytes: proof_type.context_state_size(),
|
||||
context_slot: 100,
|
||||
checks: vec!["validated".to_string()],
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_proofs_require_sysvar_and_preserve_ordered_contexts() {
|
||||
let context = pubkey(1);
|
||||
let request = crate::Token2022ProofOrchestrationRequest { operation_code: "spl_token_2022.confidential_transfer".to_string(), proofs: vec![kb_executor_spl_token_2022::SplTokenConfidentialProofReference { kind: kb_executor_spl_token_2022::SplTokenConfidentialProofKind::CiphertextCommitmentEquality, location: kb_executor_spl_token_2022::SplTokenConfidentialProofLocation::InstructionOffset { offset: 1 } }, kb_executor_spl_token_2022::SplTokenConfidentialProofReference { kind: kb_executor_spl_token_2022::SplTokenConfidentialProofKind::BatchedRangeProofU128, location: kb_executor_spl_token_2022::SplTokenConfidentialProofLocation::ContextStateAccount { account: context.clone() } }], expected_context_authority: std::option::Option::None, compute_unit_limit: 400_000, max_fee_lamports: 50_000, submit: false, operator_confirmed: false };
|
||||
let preflight = crate::Token2022CryptographicPreflightReport {
|
||||
commitment: "confirmed".to_string(),
|
||||
context_slot: 100,
|
||||
proof_contexts: vec![context_report(
|
||||
context,
|
||||
kb_executor_solana_core::ZkElGamalProofType::BatchedRangeProofU128,
|
||||
)],
|
||||
};
|
||||
let report = crate::orchestrate_token_2022_proofs(&request, &preflight)
|
||||
.expect("mixed proof orchestration must succeed");
|
||||
assert!(report.instructions_sysvar_required);
|
||||
assert_eq!(report.inline_offsets, vec![1]);
|
||||
assert_eq!(report.context_requirements.len(), 1);
|
||||
assert!(report.simulation_required);
|
||||
assert!(!report.send_authorized);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn context_only_proofs_do_not_require_instructions_sysvar() {
|
||||
let context = pubkey(2);
|
||||
let request = crate::Token2022ProofOrchestrationRequest { operation_code: "spl_token_2022.empty_confidential_account".to_string(), proofs: vec![kb_executor_spl_token_2022::SplTokenConfidentialProofReference { kind: kb_executor_spl_token_2022::SplTokenConfidentialProofKind::ZeroCiphertext, location: kb_executor_spl_token_2022::SplTokenConfidentialProofLocation::ContextStateAccount { account: context.clone() } }], expected_context_authority: std::option::Option::None, compute_unit_limit: 200_000, max_fee_lamports: 20_000, submit: true, operator_confirmed: true };
|
||||
let preflight = crate::Token2022CryptographicPreflightReport {
|
||||
commitment: "confirmed".to_string(),
|
||||
context_slot: 100,
|
||||
proof_contexts: vec![context_report(
|
||||
context,
|
||||
kb_executor_solana_core::ZkElGamalProofType::ZeroCiphertext,
|
||||
)],
|
||||
};
|
||||
let report = crate::orchestrate_token_2022_proofs(&request, &preflight)
|
||||
.expect("context-only orchestration must succeed");
|
||||
assert!(!report.instructions_sysvar_required);
|
||||
assert!(report.send_authorized);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_offsets_contexts_and_unconfirmed_submission_fail_closed() {
|
||||
let duplicate_offset = crate::Token2022ProofOrchestrationRequest { operation_code: "operation".to_string(), proofs: vec![kb_executor_spl_token_2022::SplTokenConfidentialProofReference { kind: kb_executor_spl_token_2022::SplTokenConfidentialProofKind::ZeroCiphertext, location: kb_executor_spl_token_2022::SplTokenConfidentialProofLocation::InstructionOffset { offset: 1 } }, kb_executor_spl_token_2022::SplTokenConfidentialProofReference { kind: kb_executor_spl_token_2022::SplTokenConfidentialProofKind::BatchedRangeProofU64, location: kb_executor_spl_token_2022::SplTokenConfidentialProofLocation::InstructionOffset { offset: 1 } }], expected_context_authority: std::option::Option::None, compute_unit_limit: 1, max_fee_lamports: 1, submit: false, operator_confirmed: false };
|
||||
let empty = crate::Token2022CryptographicPreflightReport {
|
||||
commitment: "confirmed".to_string(),
|
||||
context_slot: 0,
|
||||
proof_contexts: vec![],
|
||||
};
|
||||
assert!(crate::orchestrate_token_2022_proofs(&duplicate_offset, &empty).is_err());
|
||||
let mut unconfirmed = duplicate_offset;
|
||||
unconfirmed.proofs.clear();
|
||||
unconfirmed.submit = true;
|
||||
assert!(crate::orchestrate_token_2022_proofs(&unconfirmed, &empty).is_err());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,803 @@
|
||||
// file: kb_pipeline/src/solana_token_2022_stateful.rs
|
||||
// version: 5
|
||||
|
||||
//! Contextual Token-2022 account-state validation and materialization routing.
|
||||
|
||||
/// Optional external identities required to validate cross-account Token-2022 state.
|
||||
#[derive(Clone, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct Token2022StatefulContext {
|
||||
/// Expected group account for one TokenGroupMember extension.
|
||||
pub expected_group_address: std::option::Option<std::string::String>,
|
||||
}
|
||||
|
||||
/// Maximum complete Token-2022 account data accepted by one bounded RPC read.
|
||||
pub const MAX_TOKEN_2022_STATEFUL_ACCOUNT_BYTES: usize = 65_536;
|
||||
|
||||
/// One bounded Token-2022 account read request.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Token2022StatefulReadRequest {
|
||||
/// Endpoint role used for the HTTP RPC request.
|
||||
pub query_role: std::string::String,
|
||||
/// Canonical account address.
|
||||
pub account: kb_model::Pubkey,
|
||||
/// Expected Token-2022 base-state category.
|
||||
pub kind: kb_decoder_spl_token_2022::state::Token2022StateKind,
|
||||
/// Optional minimum RPC context slot.
|
||||
pub min_context_slot: std::option::Option<u64>,
|
||||
/// Maximum decoded account bytes accepted from the endpoint.
|
||||
pub max_data_bytes: usize,
|
||||
/// Optional external identities needed by cross-account extensions.
|
||||
pub context: crate::solana_token_2022_stateful::Token2022StatefulContext,
|
||||
}
|
||||
|
||||
/// One bounded RPC read and its contextually validated projections.
|
||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct Token2022StatefulReadResult {
|
||||
/// Commitment used for the RPC read.
|
||||
pub commitment: std::string::String,
|
||||
/// Context slot returned by the endpoint.
|
||||
pub context_slot: u64,
|
||||
/// Complete validated snapshot bundle.
|
||||
pub snapshot: crate::solana_token_2022_stateful::Token2022StatefulSnapshotBundle,
|
||||
}
|
||||
|
||||
/// Reads, validates, parses, and routes one bounded Token-2022 account snapshot.
|
||||
pub async fn read_token_2022_stateful_snapshot(
|
||||
pool: &kb_rpc::HttpEndpointPool,
|
||||
request: &crate::solana_token_2022_stateful::Token2022StatefulReadRequest,
|
||||
) -> kb_core::Result<crate::solana_token_2022_stateful::Token2022StatefulReadResult> {
|
||||
if request.query_role.trim().is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Token-2022 stateful read query_role must not be empty",
|
||||
));
|
||||
}
|
||||
if request.max_data_bytes == 0
|
||||
|| request.max_data_bytes
|
||||
> crate::solana_token_2022_stateful::MAX_TOKEN_2022_STATEFUL_ACCOUNT_BYTES
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::config(format!(
|
||||
"Token-2022 stateful read max_data_bytes must be between 1 and {}",
|
||||
crate::solana_token_2022_stateful::MAX_TOKEN_2022_STATEFUL_ACCOUNT_BYTES
|
||||
)));
|
||||
}
|
||||
let config = match kb_rpc::GetAccountInfoConfig::new_with_data(
|
||||
kb_rpc::RpcCommitmentLevel::Confirmed,
|
||||
request.min_context_slot,
|
||||
request.max_data_bytes,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let result = match pool
|
||||
.get_account_info_for_role(request.query_role.as_str(), &request.account, &config)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return crate::solana_token_2022_stateful::materialize_token_2022_account_info_result(
|
||||
request, &result,
|
||||
);
|
||||
}
|
||||
|
||||
/// Validates one complete RPC account response before Token-2022 parsing and routing.
|
||||
pub fn materialize_token_2022_account_info_result(
|
||||
request: &crate::solana_token_2022_stateful::Token2022StatefulReadRequest,
|
||||
result: &kb_rpc::AccountInfoResult,
|
||||
) -> kb_core::Result<crate::solana_token_2022_stateful::Token2022StatefulReadResult> {
|
||||
if request.max_data_bytes == 0
|
||||
|| request.max_data_bytes
|
||||
> crate::solana_token_2022_stateful::MAX_TOKEN_2022_STATEFUL_ACCOUNT_BYTES
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::config(format!(
|
||||
"Token-2022 stateful read max_data_bytes must be between 1 and {}",
|
||||
crate::solana_token_2022_stateful::MAX_TOKEN_2022_STATEFUL_ACCOUNT_BYTES
|
||||
)));
|
||||
}
|
||||
if let std::option::Option::Some(min_context_slot) = request.min_context_slot {
|
||||
if result.context.slot < min_context_slot {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_stateful_context_slot_too_old",
|
||||
format!(
|
||||
"Token-2022 account context slot {} is below requested minimum {min_context_slot}",
|
||||
result.context.slot
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
let account = match result.account.as_ref() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_stateful_account_missing",
|
||||
format!("Token-2022 account {} does not exist", request.account.0),
|
||||
));
|
||||
},
|
||||
};
|
||||
if account.executable {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_stateful_account_executable",
|
||||
format!("Token-2022 state account {} must not be executable", request.account.0),
|
||||
));
|
||||
}
|
||||
if account.owner.0 != kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_stateful_owner_mismatch",
|
||||
format!(
|
||||
"Token-2022 state account {} owner must be {}, got {}",
|
||||
request.account.0,
|
||||
kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID,
|
||||
account.owner.0
|
||||
),
|
||||
));
|
||||
}
|
||||
if account.space > request.max_data_bytes as u64 || account.data.len() > request.max_data_bytes
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_stateful_account_too_large",
|
||||
format!(
|
||||
"Token-2022 account {} reports {} bytes and returned {} bytes above limit {}",
|
||||
request.account.0,
|
||||
account.space,
|
||||
account.data.len(),
|
||||
request.max_data_bytes
|
||||
),
|
||||
));
|
||||
}
|
||||
if account.space != account.data.len() as u64 {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_stateful_account_data_incomplete",
|
||||
format!(
|
||||
"Token-2022 account {} reports {} bytes but returned {} decoded bytes",
|
||||
request.account.0,
|
||||
account.space,
|
||||
account.data.len()
|
||||
),
|
||||
));
|
||||
}
|
||||
let state = match kb_decoder_spl_token_2022::state::parse_token_2022_state(
|
||||
request.kind,
|
||||
account.data.as_slice(),
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_stateful_parse_failed",
|
||||
error,
|
||||
));
|
||||
},
|
||||
};
|
||||
let snapshot = match crate::solana_token_2022_stateful::materialize_parsed_token_2022_stateful_snapshot_with_context(
|
||||
request.account.0.as_str(),
|
||||
result.context.slot,
|
||||
&state,
|
||||
&request.context,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_stateful_projection_failed",
|
||||
error,
|
||||
));
|
||||
},
|
||||
};
|
||||
return std::result::Result::Ok(
|
||||
crate::solana_token_2022_stateful::Token2022StatefulReadResult {
|
||||
commitment: "confirmed".to_string(),
|
||||
context_slot: result.context.slot,
|
||||
snapshot,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// One contextually validated Token-2022 state snapshot and its owned projections.
|
||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct Token2022StatefulSnapshotBundle {
|
||||
/// Canonical account identity used for contextual checks and stable output keys.
|
||||
pub account_key: std::string::String,
|
||||
/// Context slot associated with the account read.
|
||||
pub slot: u64,
|
||||
/// Parsed base-state category.
|
||||
pub state_kind: std::string::String,
|
||||
/// Ordered published or future extension names retained by the parser.
|
||||
pub extension_names: std::vec::Vec<std::string::String>,
|
||||
/// Processor-owned projections routed without duplicate ownership.
|
||||
pub outputs: std::vec::Vec<kb_materializer_api::MaterializedOutput>,
|
||||
}
|
||||
|
||||
/// Parse, contextually validate, and materialize one bounded Token-2022 account snapshot.
|
||||
pub fn materialize_token_2022_stateful_snapshot(
|
||||
account_key: &str,
|
||||
owner_program_id: &str,
|
||||
slot: u64,
|
||||
kind: kb_decoder_spl_token_2022::state::Token2022StateKind,
|
||||
data: &[u8],
|
||||
) -> std::result::Result<crate::solana_token_2022_stateful::Token2022StatefulSnapshotBundle, String>
|
||||
{
|
||||
if owner_program_id != kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID {
|
||||
return std::result::Result::Err(format!(
|
||||
"Token-2022 state snapshot owner must be {}, got {owner_program_id}",
|
||||
kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID
|
||||
));
|
||||
}
|
||||
let state = match kb_decoder_spl_token_2022::state::parse_token_2022_state(kind, data) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return crate::solana_token_2022_stateful::materialize_parsed_token_2022_stateful_snapshot_with_context(
|
||||
account_key,
|
||||
slot,
|
||||
&state,
|
||||
&crate::solana_token_2022_stateful::Token2022StatefulContext::default(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Contextually validate and materialize one already parsed Token-2022 account snapshot.
|
||||
pub fn materialize_parsed_token_2022_stateful_snapshot(
|
||||
account_key: &str,
|
||||
slot: u64,
|
||||
state: &kb_decoder_spl_token_2022::state::Token2022State,
|
||||
) -> std::result::Result<crate::solana_token_2022_stateful::Token2022StatefulSnapshotBundle, String>
|
||||
{
|
||||
return crate::solana_token_2022_stateful::materialize_parsed_token_2022_stateful_snapshot_with_context(
|
||||
account_key,
|
||||
slot,
|
||||
state,
|
||||
&crate::solana_token_2022_stateful::Token2022StatefulContext::default(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Contextually validate one parsed Token-2022 snapshot with external cross-account identities.
|
||||
pub fn materialize_parsed_token_2022_stateful_snapshot_with_context(
|
||||
account_key: &str,
|
||||
slot: u64,
|
||||
state: &kb_decoder_spl_token_2022::state::Token2022State,
|
||||
context: &crate::solana_token_2022_stateful::Token2022StatefulContext,
|
||||
) -> std::result::Result<crate::solana_token_2022_stateful::Token2022StatefulSnapshotBundle, String>
|
||||
{
|
||||
if account_key.trim().is_empty() {
|
||||
return std::result::Result::Err(
|
||||
"Token-2022 stateful snapshot requires a non-empty account key".to_string(),
|
||||
);
|
||||
}
|
||||
let decoded = match bs58::decode(account_key).into_vec() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(
|
||||
"Token-2022 stateful snapshot account key must be valid base58".to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
if decoded.len() != 32 {
|
||||
return std::result::Result::Err(format!(
|
||||
"Token-2022 stateful snapshot account key must decode to 32 bytes, got {}",
|
||||
decoded.len()
|
||||
));
|
||||
}
|
||||
match crate::solana_token_2022_stateful::validate_embedded_mint_identity(account_key, state) {
|
||||
std::result::Result::Ok(()) => {},
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
match crate::solana_token_2022_stateful::validate_group_member_identity(state, context) {
|
||||
std::result::Result::Ok(()) => {},
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
let account_output = match kb_materializer_token_accounts::materialize_token_2022_state_snapshot(
|
||||
account_key,
|
||||
slot,
|
||||
state,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mut outputs = std::vec![account_output];
|
||||
if state.kind == kb_decoder_spl_token_2022::state::Token2022StateKind::Mint {
|
||||
let metadata_outputs =
|
||||
match kb_materializer_metadata::materialize_token_2022_metadata_snapshot(
|
||||
account_key,
|
||||
slot,
|
||||
state,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
outputs.extend(metadata_outputs);
|
||||
}
|
||||
let fee_outputs = match kb_materializer_fees::materialize_token_2022_fee_state_snapshots(
|
||||
account_key,
|
||||
slot,
|
||||
state,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
outputs.extend(fee_outputs);
|
||||
let admin_outputs = match kb_materializer_admin::materialize_token_2022_admin_state_snapshots(
|
||||
account_key,
|
||||
slot,
|
||||
state,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
outputs.extend(admin_outputs);
|
||||
let state_kind = match state.kind {
|
||||
kb_decoder_spl_token_2022::state::Token2022StateKind::Mint => "mint",
|
||||
kb_decoder_spl_token_2022::state::Token2022StateKind::Account => "account",
|
||||
kb_decoder_spl_token_2022::state::Token2022StateKind::Multisig => "multisig",
|
||||
};
|
||||
return std::result::Result::Ok(
|
||||
crate::solana_token_2022_stateful::Token2022StatefulSnapshotBundle {
|
||||
account_key: account_key.to_string(),
|
||||
slot,
|
||||
state_kind: state_kind.to_string(),
|
||||
extension_names: state
|
||||
.extensions
|
||||
.iter()
|
||||
.map(|entry| return entry.extension_name.to_string())
|
||||
.collect(),
|
||||
outputs,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn validate_embedded_mint_identity(
|
||||
account_key: &str,
|
||||
state: &kb_decoder_spl_token_2022::state::Token2022State,
|
||||
) -> std::result::Result<(), String> {
|
||||
for entry in &state.extensions {
|
||||
if entry.extension_name != "token_metadata"
|
||||
&& entry.extension_name != "token_group"
|
||||
&& entry.extension_name != "token_group_member"
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let embedded_mint = entry.value_fields.get("mint").and_then(serde_json::Value::as_str);
|
||||
let embedded_mint = match embedded_mint {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(format!(
|
||||
"Token-2022 {} extension requires a structured mint field",
|
||||
entry.extension_name
|
||||
));
|
||||
},
|
||||
};
|
||||
if embedded_mint != account_key {
|
||||
return std::result::Result::Err(format!(
|
||||
"Token-2022 {} mint {} does not match account {account_key}",
|
||||
entry.extension_name, embedded_mint
|
||||
));
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn validate_group_member_identity(
|
||||
state: &kb_decoder_spl_token_2022::state::Token2022State,
|
||||
context: &crate::solana_token_2022_stateful::Token2022StatefulContext,
|
||||
) -> std::result::Result<(), String> {
|
||||
for entry in &state.extensions {
|
||||
if entry.extension_name != "token_group_member" {
|
||||
continue;
|
||||
}
|
||||
let group = entry.value_fields.get("group").and_then(serde_json::Value::as_str);
|
||||
let group = match group {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(
|
||||
"Token-2022 token_group_member extension requires a structured group field"
|
||||
.to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
let expected = match context.expected_group_address.as_deref() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(
|
||||
"Token-2022 token_group_member validation requires an expected group address"
|
||||
.to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
if group != expected {
|
||||
return std::result::Result::Err(format!(
|
||||
"Token-2022 token_group_member group {group} does not match expected group {expected}"
|
||||
));
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
fn account_key(byte: u8) -> String {
|
||||
return bs58::encode([byte; 32]).into_string();
|
||||
}
|
||||
|
||||
fn mint_state(account_key: &str) -> kb_decoder_spl_token_2022::state::Token2022State {
|
||||
return kb_decoder_spl_token_2022::state::Token2022State {
|
||||
kind: kb_decoder_spl_token_2022::state::Token2022StateKind::Mint,
|
||||
base_fields: serde_json::json!({"supply":"1","decimals":0,"initialized":true}),
|
||||
base_hex: "00".repeat(82),
|
||||
account_type: std::option::Option::Some(1),
|
||||
extensions: std::vec![
|
||||
kb_decoder_spl_token_2022::state::Token2022TlvEntry {
|
||||
extension_type: 19,
|
||||
extension_name: "token_metadata",
|
||||
value_hex: "01".to_string(),
|
||||
value_fields: serde_json::json!({
|
||||
"mint": account_key,
|
||||
"name": "Token",
|
||||
"symbol": "TOK",
|
||||
"uri": "https://example.invalid/token.json"
|
||||
}),
|
||||
},
|
||||
kb_decoder_spl_token_2022::state::Token2022TlvEntry {
|
||||
extension_type: 20,
|
||||
extension_name: "token_group",
|
||||
value_hex: "02".to_string(),
|
||||
value_fields: serde_json::json!({
|
||||
"mint": account_key,
|
||||
"size": "1",
|
||||
"maxSize": "10"
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mint_snapshot_routes_account_and_metadata_outputs_once() {
|
||||
let account_key = account_key(7);
|
||||
let state = mint_state(account_key.as_str());
|
||||
let bundle =
|
||||
crate::solana_token_2022_stateful::materialize_parsed_token_2022_stateful_snapshot(
|
||||
account_key.as_str(),
|
||||
42,
|
||||
&state,
|
||||
);
|
||||
assert_eq!(
|
||||
bundle.as_ref().map(|value| return value.outputs.len()),
|
||||
std::result::Result::Ok(3)
|
||||
);
|
||||
assert_eq!(
|
||||
bundle.as_ref().map(|value| return value.state_kind.clone()),
|
||||
std::result::Result::Ok("mint".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
bundle.as_ref().map(|value| return value.extension_names.clone()),
|
||||
std::result::Result::Ok(std::vec![
|
||||
"token_metadata".to_string(),
|
||||
"token_group".to_string()
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embedded_metadata_mint_must_match_the_account_identity() {
|
||||
let account_address = account_key(8);
|
||||
let mut state = mint_state(account_address.as_str());
|
||||
state.extensions[0].value_fields["mint"] = serde_json::json!(account_key(9));
|
||||
let result =
|
||||
crate::solana_token_2022_stateful::materialize_parsed_token_2022_stateful_snapshot(
|
||||
account_address.as_str(),
|
||||
42,
|
||||
&state,
|
||||
);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn owner_and_account_identity_fail_closed_before_projection() {
|
||||
let account_key = account_key(10);
|
||||
let data = [0u8; 82];
|
||||
let wrong_owner =
|
||||
crate::solana_token_2022_stateful::materialize_token_2022_stateful_snapshot(
|
||||
account_key.as_str(),
|
||||
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
1,
|
||||
kb_decoder_spl_token_2022::state::Token2022StateKind::Mint,
|
||||
&data,
|
||||
);
|
||||
assert!(wrong_owner.is_err());
|
||||
let state = mint_state(account_key.as_str());
|
||||
let malformed_key =
|
||||
crate::solana_token_2022_stateful::materialize_parsed_token_2022_stateful_snapshot(
|
||||
"not-base58-0",
|
||||
1,
|
||||
&state,
|
||||
);
|
||||
assert!(malformed_key.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_snapshot_has_no_metadata_projection() {
|
||||
let account_key = account_key(11);
|
||||
let state = kb_decoder_spl_token_2022::state::Token2022State {
|
||||
kind: kb_decoder_spl_token_2022::state::Token2022StateKind::Account,
|
||||
base_fields: serde_json::json!({"amount":"0","state":"initialized"}),
|
||||
base_hex: "00".repeat(165),
|
||||
account_type: std::option::Option::None,
|
||||
extensions: std::vec::Vec::new(),
|
||||
};
|
||||
let bundle =
|
||||
crate::solana_token_2022_stateful::materialize_parsed_token_2022_stateful_snapshot(
|
||||
account_key.as_str(),
|
||||
9,
|
||||
&state,
|
||||
);
|
||||
assert_eq!(
|
||||
bundle.as_ref().map(|value| return value.outputs.len()),
|
||||
std::result::Result::Ok(1)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn group_member_requires_and_matches_external_group_identity() {
|
||||
let account_address = account_key(12);
|
||||
let group_address = account_key(13);
|
||||
let state = kb_decoder_spl_token_2022::state::Token2022State {
|
||||
kind: kb_decoder_spl_token_2022::state::Token2022StateKind::Mint,
|
||||
base_fields: serde_json::json!({"supply":"1","decimals":0,"initialized":true}),
|
||||
base_hex: "00".repeat(82),
|
||||
account_type: std::option::Option::Some(1),
|
||||
extensions: std::vec![kb_decoder_spl_token_2022::state::Token2022TlvEntry {
|
||||
extension_type: 23,
|
||||
extension_name: "token_group_member",
|
||||
value_hex: "03".to_string(),
|
||||
value_fields: serde_json::json!({
|
||||
"mint": account_address,
|
||||
"group": group_address,
|
||||
"memberNumber": "1"
|
||||
}),
|
||||
}],
|
||||
};
|
||||
let missing =
|
||||
crate::solana_token_2022_stateful::materialize_parsed_token_2022_stateful_snapshot(
|
||||
account_address.as_str(),
|
||||
44,
|
||||
&state,
|
||||
);
|
||||
assert!(missing.is_err());
|
||||
let wrong_context = crate::solana_token_2022_stateful::Token2022StatefulContext {
|
||||
expected_group_address: std::option::Option::Some(account_key(14)),
|
||||
};
|
||||
let wrong = crate::solana_token_2022_stateful::materialize_parsed_token_2022_stateful_snapshot_with_context(
|
||||
account_address.as_str(),
|
||||
44,
|
||||
&state,
|
||||
&wrong_context,
|
||||
);
|
||||
assert!(wrong.is_err());
|
||||
let context = crate::solana_token_2022_stateful::Token2022StatefulContext {
|
||||
expected_group_address: std::option::Option::Some(group_address),
|
||||
};
|
||||
let valid = crate::solana_token_2022_stateful::materialize_parsed_token_2022_stateful_snapshot_with_context(
|
||||
account_address.as_str(),
|
||||
44,
|
||||
&state,
|
||||
&context,
|
||||
);
|
||||
assert_eq!(
|
||||
valid.as_ref().map(|bundle| return bundle.outputs.len()),
|
||||
std::result::Result::Ok(2)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn mint_and_account_fee_extensions_route_to_the_fee_owner_once() {
|
||||
let mint_address = account_key(15);
|
||||
let mint = kb_decoder_spl_token_2022::state::Token2022State {
|
||||
kind: kb_decoder_spl_token_2022::state::Token2022StateKind::Mint,
|
||||
base_fields: serde_json::json!({"supply":"1","decimals":0,"initialized":true}),
|
||||
base_hex: "00".repeat(82),
|
||||
account_type: std::option::Option::Some(1),
|
||||
extensions: std::vec![kb_decoder_spl_token_2022::state::Token2022TlvEntry {
|
||||
extension_type: 1,
|
||||
extension_name: "transfer_fee_config",
|
||||
value_hex: "01".to_string(),
|
||||
value_fields: serde_json::json!({"withheldAmount":"7"}),
|
||||
}],
|
||||
};
|
||||
let mint_bundle =
|
||||
crate::solana_token_2022_stateful::materialize_parsed_token_2022_stateful_snapshot(
|
||||
mint_address.as_str(),
|
||||
50,
|
||||
&mint,
|
||||
);
|
||||
assert_eq!(
|
||||
mint_bundle.as_ref().map(|bundle| return bundle.outputs.len()),
|
||||
std::result::Result::Ok(2)
|
||||
);
|
||||
assert_eq!(
|
||||
mint_bundle
|
||||
.as_ref()
|
||||
.map(|bundle| return bundle.outputs[1].payload_json["provenance"]["processorName"]
|
||||
.clone()),
|
||||
std::result::Result::Ok(serde_json::json!("fees"))
|
||||
);
|
||||
let account_address = account_key(16);
|
||||
let account = kb_decoder_spl_token_2022::state::Token2022State {
|
||||
kind: kb_decoder_spl_token_2022::state::Token2022StateKind::Account,
|
||||
base_fields: serde_json::json!({"amount":"0","state":"initialized"}),
|
||||
base_hex: "00".repeat(165),
|
||||
account_type: std::option::Option::Some(2),
|
||||
extensions: std::vec![kb_decoder_spl_token_2022::state::Token2022TlvEntry {
|
||||
extension_type: 17,
|
||||
extension_name: "confidential_transfer_fee_amount",
|
||||
value_hex: "02".to_string(),
|
||||
value_fields: serde_json::json!({"withheldAmount":"ciphertext"}),
|
||||
}],
|
||||
};
|
||||
let account_bundle =
|
||||
crate::solana_token_2022_stateful::materialize_parsed_token_2022_stateful_snapshot(
|
||||
account_address.as_str(),
|
||||
51,
|
||||
&account,
|
||||
);
|
||||
assert_eq!(
|
||||
account_bundle.as_ref().map(|bundle| return bundle.outputs.len()),
|
||||
std::result::Result::Ok(2)
|
||||
);
|
||||
assert_eq!(
|
||||
account_bundle
|
||||
.as_ref()
|
||||
.map(|bundle| return bundle.outputs[1].payload_json["confidentialValuesDecrypted"]
|
||||
.clone()),
|
||||
std::result::Result::Ok(serde_json::json!(false))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mint_admin_extensions_route_to_the_admin_owner_once() {
|
||||
let account_key = account_key(12);
|
||||
let mut state = mint_state(account_key.as_str());
|
||||
state.extensions.push(kb_decoder_spl_token_2022::state::Token2022TlvEntry {
|
||||
extension_type: 6,
|
||||
extension_name: "default_account_state",
|
||||
value_hex: "02".to_string(),
|
||||
value_fields: serde_json::json!({"state": 2}),
|
||||
});
|
||||
let bundle =
|
||||
crate::solana_token_2022_stateful::materialize_parsed_token_2022_stateful_snapshot(
|
||||
account_key.as_str(),
|
||||
17,
|
||||
&state,
|
||||
);
|
||||
assert_eq!(
|
||||
bundle.as_ref().map(|value| return value.outputs.len()),
|
||||
std::result::Result::Ok(4)
|
||||
);
|
||||
assert_eq!(
|
||||
bundle.as_ref().map(|value| {
|
||||
return value
|
||||
.outputs
|
||||
.iter()
|
||||
.filter(|output| {
|
||||
return output.payload_json["domain"]
|
||||
== serde_json::json!("token_2022_extension_admin_state");
|
||||
})
|
||||
.count();
|
||||
}),
|
||||
std::result::Result::Ok(1)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_rpc_account_routes_only_after_owner_size_and_context_validation() {
|
||||
let account = crate::solana_token_2022_stateful::tests::account_key(21);
|
||||
let request = crate::solana_token_2022_stateful::Token2022StatefulReadRequest {
|
||||
query_role: "execution".to_string(),
|
||||
account: kb_model::Pubkey(account.clone()),
|
||||
kind: kb_decoder_spl_token_2022::state::Token2022StateKind::Mint,
|
||||
min_context_slot: std::option::Option::Some(40),
|
||||
max_data_bytes: 82,
|
||||
context: crate::solana_token_2022_stateful::Token2022StatefulContext::default(),
|
||||
};
|
||||
let result = kb_rpc::AccountInfoResult {
|
||||
context: kb_rpc::RpcResponseContext {
|
||||
slot: 41,
|
||||
api_version: std::option::Option::None,
|
||||
},
|
||||
account: std::option::Option::Some(kb_rpc::AccountInfoValue {
|
||||
lamports: 1,
|
||||
owner: kb_model::ProgramId(kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID.to_string()),
|
||||
executable: false,
|
||||
rent_epoch: 0,
|
||||
space: 82,
|
||||
data: std::vec![0; 82],
|
||||
}),
|
||||
};
|
||||
let materialized =
|
||||
crate::solana_token_2022_stateful::materialize_token_2022_account_info_result(
|
||||
&request, &result,
|
||||
);
|
||||
assert_eq!(
|
||||
materialized.as_ref().map(|value| return value.context_slot),
|
||||
std::result::Result::Ok(41)
|
||||
);
|
||||
assert_eq!(
|
||||
materialized.as_ref().map(|value| return value.snapshot.outputs.len()),
|
||||
std::result::Result::Ok(1)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn incomplete_foreign_executable_and_stale_rpc_accounts_fail_closed() {
|
||||
let account = crate::solana_token_2022_stateful::tests::account_key(22);
|
||||
let request = crate::solana_token_2022_stateful::Token2022StatefulReadRequest {
|
||||
query_role: "execution".to_string(),
|
||||
account: kb_model::Pubkey(account),
|
||||
kind: kb_decoder_spl_token_2022::state::Token2022StateKind::Mint,
|
||||
min_context_slot: std::option::Option::Some(50),
|
||||
max_data_bytes: 82,
|
||||
context: crate::solana_token_2022_stateful::Token2022StatefulContext::default(),
|
||||
};
|
||||
let base = kb_rpc::AccountInfoValue {
|
||||
lamports: 1,
|
||||
owner: kb_model::ProgramId(kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID.to_string()),
|
||||
executable: false,
|
||||
rent_epoch: 0,
|
||||
space: 82,
|
||||
data: std::vec![0; 82],
|
||||
};
|
||||
let stale = kb_rpc::AccountInfoResult {
|
||||
context: kb_rpc::RpcResponseContext {
|
||||
slot: 49,
|
||||
api_version: std::option::Option::None,
|
||||
},
|
||||
account: std::option::Option::Some(base.clone()),
|
||||
};
|
||||
assert!(
|
||||
crate::solana_token_2022_stateful::materialize_token_2022_account_info_result(
|
||||
&request, &stale
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
let mut foreign = base.clone();
|
||||
foreign.owner = kb_model::ProgramId(kb_program_ids::SPL_TOKEN_PROGRAM_ID.to_string());
|
||||
let foreign = kb_rpc::AccountInfoResult {
|
||||
context: kb_rpc::RpcResponseContext {
|
||||
slot: 50,
|
||||
api_version: std::option::Option::None,
|
||||
},
|
||||
account: std::option::Option::Some(foreign),
|
||||
};
|
||||
assert!(
|
||||
crate::solana_token_2022_stateful::materialize_token_2022_account_info_result(
|
||||
&request, &foreign
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
let mut executable = base.clone();
|
||||
executable.executable = true;
|
||||
let executable = kb_rpc::AccountInfoResult {
|
||||
context: kb_rpc::RpcResponseContext {
|
||||
slot: 50,
|
||||
api_version: std::option::Option::None,
|
||||
},
|
||||
account: std::option::Option::Some(executable),
|
||||
};
|
||||
assert!(
|
||||
crate::solana_token_2022_stateful::materialize_token_2022_account_info_result(
|
||||
&request,
|
||||
&executable
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
let mut incomplete = base;
|
||||
incomplete.data.pop();
|
||||
let incomplete = kb_rpc::AccountInfoResult {
|
||||
context: kb_rpc::RpcResponseContext {
|
||||
slot: 50,
|
||||
api_version: std::option::Option::None,
|
||||
},
|
||||
account: std::option::Option::Some(incomplete),
|
||||
};
|
||||
assert!(
|
||||
crate::solana_token_2022_stateful::materialize_token_2022_account_info_result(
|
||||
&request,
|
||||
&incomplete
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,440 @@
|
||||
// file: kb_pipeline/src/solana_token_2022_validation.rs
|
||||
// version: 2
|
||||
|
||||
//! Machine-readable validation evidence contract for the Token-2022 milestone.
|
||||
|
||||
/// Maximum number of evidence records accepted in one validation report.
|
||||
pub const MAX_TOKEN_2022_VALIDATION_EVIDENCE: usize = 64;
|
||||
|
||||
/// Required validation environment for one scenario.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Token2022ValidationEnvironment {
|
||||
/// Deterministic offline fixtures and builder comparisons.
|
||||
Offline,
|
||||
/// A local validator under operator control.
|
||||
Localnet,
|
||||
/// Solana Devnet.
|
||||
Devnet,
|
||||
/// Scenario may run on Localnet or Devnet according to deployment availability.
|
||||
LocalnetOrDevnet,
|
||||
/// Mainnet observations without mutable execution.
|
||||
MainnetObservation,
|
||||
/// PostgreSQL persistence and replay validation.
|
||||
Postgres,
|
||||
/// Tauri application smoke validation.
|
||||
Tauri,
|
||||
}
|
||||
|
||||
/// Exact status of one validation scenario.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Token2022ValidationStatus {
|
||||
/// Scenario is declared but has not been run.
|
||||
NotRun,
|
||||
/// Scenario was simulated without submission.
|
||||
Simulated,
|
||||
/// Scenario was submitted but confirmation evidence is incomplete.
|
||||
Submitted,
|
||||
/// Scenario was confirmed and its required postconditions passed.
|
||||
Confirmed,
|
||||
/// Scenario is not available in the selected environment.
|
||||
Unavailable,
|
||||
/// Scenario ran and failed.
|
||||
Failed,
|
||||
}
|
||||
|
||||
/// One bounded proof attached to a validation scenario.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct Token2022ValidationEvidence {
|
||||
/// Stable evidence kind such as `signature`, `test_suite`, or `replay`.
|
||||
pub kind: std::string::String,
|
||||
/// Bounded evidence value.
|
||||
pub value: std::string::String,
|
||||
}
|
||||
|
||||
/// One declared Token-2022 validation scenario and its observed evidence.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct Token2022ValidationScenario {
|
||||
/// Stable scenario identifier.
|
||||
pub id: std::string::String,
|
||||
/// Validation environment.
|
||||
pub environment: crate::Token2022ValidationEnvironment,
|
||||
/// Exact observed status.
|
||||
pub status: crate::Token2022ValidationStatus,
|
||||
/// Whether canonical hydration is required.
|
||||
pub requires_canonical_hydration: bool,
|
||||
/// Whether core extraction is required.
|
||||
pub requires_core_extraction: bool,
|
||||
/// Whether decode replay is required.
|
||||
pub requires_decode_replay: bool,
|
||||
/// Whether materialization is required.
|
||||
pub requires_materialization: bool,
|
||||
/// Whether a second idempotent replay is required.
|
||||
pub requires_second_replay: bool,
|
||||
/// Ordered evidence records.
|
||||
pub evidence: std::vec::Vec<crate::Token2022ValidationEvidence>,
|
||||
}
|
||||
|
||||
/// Machine-readable validation matrix loaded from the canonical JSON document.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Token2022ValidationMatrix {
|
||||
/// Matrix schema version.
|
||||
pub matrix_version: u32,
|
||||
/// Milestone owning this matrix.
|
||||
pub milestone: std::string::String,
|
||||
/// Aggregate matrix status.
|
||||
pub status: std::string::String,
|
||||
/// Exact accepted status vocabulary.
|
||||
pub status_vocabulary: std::vec::Vec<std::string::String>,
|
||||
/// Scenarios in stable roadmap order.
|
||||
pub scenarios: std::vec::Vec<crate::Token2022ValidationMatrixScenario>,
|
||||
}
|
||||
|
||||
/// One scenario declared by the canonical validation matrix.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Token2022ValidationMatrixScenario {
|
||||
/// Stable scenario identifier.
|
||||
pub id: std::string::String,
|
||||
/// Required execution environment.
|
||||
pub environment: crate::Token2022ValidationEnvironment,
|
||||
/// Exact observed status.
|
||||
pub status: crate::Token2022ValidationStatus,
|
||||
/// Evidence kinds required before this scenario may be confirmed.
|
||||
pub required_evidence: std::vec::Vec<std::string::String>,
|
||||
/// Observed bounded evidence.
|
||||
#[serde(default)]
|
||||
pub evidence: std::vec::Vec<crate::Token2022ValidationEvidence>,
|
||||
}
|
||||
|
||||
/// Loads and validates the canonical Token-2022 validation matrix.
|
||||
pub fn load_token_2022_validation_matrix() -> kb_core::Result<crate::Token2022ValidationMatrix> {
|
||||
let parsed = match serde_json::from_str::<crate::Token2022ValidationMatrix>(include_str!(
|
||||
"../../docs/SPL_TOKEN_2022_VALIDATION_MATRIX.json"
|
||||
)) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_validation_matrix_invalid_json",
|
||||
format!("Token-2022 validation matrix JSON is invalid: {error}"),
|
||||
));
|
||||
},
|
||||
};
|
||||
if let std::result::Result::Err(error) = validate_token_2022_validation_matrix(&parsed) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
return std::result::Result::Ok(parsed);
|
||||
}
|
||||
|
||||
/// Validates schema, scenario inventory, statuses, and observed evidence.
|
||||
pub fn validate_token_2022_validation_matrix(
|
||||
matrix: &crate::Token2022ValidationMatrix,
|
||||
) -> kb_core::Result<()> {
|
||||
if matrix.matrix_version != 2 || matrix.milestone != "0.4.6" {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_validation_matrix_contract_mismatch",
|
||||
"Token-2022 validation matrix must use version 2 for milestone 0.4.6",
|
||||
));
|
||||
}
|
||||
let expected_statuses =
|
||||
["not_run", "simulated", "submitted", "confirmed", "unavailable", "failed"];
|
||||
let actual_statuses = matrix
|
||||
.status_vocabulary
|
||||
.iter()
|
||||
.map(|status| return status.as_str())
|
||||
.collect::<std::vec::Vec<&str>>();
|
||||
if actual_statuses.as_slice() != expected_statuses {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_validation_matrix_status_vocabulary_mismatch",
|
||||
"Token-2022 validation status vocabulary differs from the compiled contract",
|
||||
));
|
||||
}
|
||||
let expected_ids = [
|
||||
"offline_full_regression",
|
||||
"token_2022_public_devnet",
|
||||
"elgamal_registry_devnet",
|
||||
"confidential_transfer_localnet_or_devnet",
|
||||
"confidential_mint_burn_localnet_or_devnet",
|
||||
"permissioned_confidential_burn_localnet_or_devnet",
|
||||
"mainnet_observation_corpus",
|
||||
"postgres_double_replay",
|
||||
"tauri_smoke",
|
||||
];
|
||||
let actual_ids = matrix
|
||||
.scenarios
|
||||
.iter()
|
||||
.map(|scenario| return scenario.id.as_str())
|
||||
.collect::<std::vec::Vec<&str>>();
|
||||
if actual_ids.as_slice() != expected_ids {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_validation_matrix_scenario_inventory_mismatch",
|
||||
"Token-2022 validation scenario inventory or order differs from the compiled contract",
|
||||
));
|
||||
}
|
||||
for scenario in &matrix.scenarios {
|
||||
if scenario.required_evidence.is_empty()
|
||||
|| scenario.required_evidence.len() > crate::MAX_TOKEN_2022_VALIDATION_EVIDENCE
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_validation_matrix_required_evidence_invalid",
|
||||
"Every Token-2022 validation scenario must declare bounded required evidence",
|
||||
));
|
||||
}
|
||||
let mut required = std::collections::BTreeSet::<&str>::new();
|
||||
for evidence_kind in &scenario.required_evidence {
|
||||
if evidence_kind.trim().is_empty() || !required.insert(evidence_kind.as_str()) {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_validation_matrix_required_evidence_invalid",
|
||||
"Required Token-2022 validation evidence must be non-empty and unique",
|
||||
));
|
||||
}
|
||||
}
|
||||
if scenario.status == crate::Token2022ValidationStatus::Confirmed {
|
||||
let observed = scenario
|
||||
.evidence
|
||||
.iter()
|
||||
.map(|evidence| return evidence.kind.as_str())
|
||||
.collect::<std::collections::BTreeSet<&str>>();
|
||||
if !required.iter().all(|kind| return observed.contains(kind)) {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_validation_matrix_confirmed_without_required_evidence",
|
||||
format!(
|
||||
"Confirmed Token-2022 validation scenario {} lacks required evidence",
|
||||
scenario.id
|
||||
),
|
||||
));
|
||||
}
|
||||
} else if matches!(
|
||||
scenario.status,
|
||||
crate::Token2022ValidationStatus::NotRun
|
||||
| crate::Token2022ValidationStatus::Unavailable
|
||||
) && !scenario.evidence.is_empty()
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_validation_matrix_unobserved_with_evidence",
|
||||
"Not-run or unavailable Token-2022 scenarios must not retain observed evidence",
|
||||
));
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
/// Complete validation report checked before milestone closure.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct Token2022ValidationReport {
|
||||
/// Scenarios in stable roadmap order.
|
||||
pub scenarios: std::vec::Vec<crate::Token2022ValidationScenario>,
|
||||
}
|
||||
|
||||
/// Validates that a milestone report is bounded, unique, and does not overclaim evidence.
|
||||
pub fn validate_token_2022_validation_report(
|
||||
report: &crate::Token2022ValidationReport,
|
||||
) -> kb_core::Result<()> {
|
||||
if report.scenarios.is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Token-2022 validation report must contain at least one scenario",
|
||||
));
|
||||
}
|
||||
let mut ids = std::collections::BTreeSet::<std::string::String>::new();
|
||||
for scenario in &report.scenarios {
|
||||
if scenario.id.trim().is_empty() || !ids.insert(scenario.id.clone()) {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_validation_scenario_identity_invalid",
|
||||
"Token-2022 validation scenario ids must be non-empty and unique",
|
||||
));
|
||||
}
|
||||
if scenario.evidence.len() > crate::MAX_TOKEN_2022_VALIDATION_EVIDENCE {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_validation_evidence_limit_exceeded",
|
||||
format!(
|
||||
"Token-2022 validation accepts at most {} evidence records per scenario",
|
||||
crate::MAX_TOKEN_2022_VALIDATION_EVIDENCE
|
||||
),
|
||||
));
|
||||
}
|
||||
for evidence in &scenario.evidence {
|
||||
if evidence.kind.trim().is_empty() || evidence.value.trim().is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_validation_evidence_invalid",
|
||||
"Token-2022 validation evidence kind and value must be non-empty",
|
||||
));
|
||||
}
|
||||
}
|
||||
let completed = matches!(
|
||||
scenario.status,
|
||||
crate::Token2022ValidationStatus::Simulated
|
||||
| crate::Token2022ValidationStatus::Submitted
|
||||
| crate::Token2022ValidationStatus::Confirmed
|
||||
| crate::Token2022ValidationStatus::Failed
|
||||
);
|
||||
if completed && scenario.evidence.is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_validation_completed_without_evidence",
|
||||
"A completed Token-2022 validation scenario must retain evidence",
|
||||
));
|
||||
}
|
||||
if scenario.status == crate::Token2022ValidationStatus::Confirmed
|
||||
&& (scenario.requires_canonical_hydration
|
||||
|| scenario.requires_core_extraction
|
||||
|| scenario.requires_decode_replay
|
||||
|| scenario.requires_materialization
|
||||
|| scenario.requires_second_replay)
|
||||
&& !has_pipeline_evidence(scenario)
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_validation_confirmed_without_pipeline_evidence",
|
||||
"A confirmed end-to-end Token-2022 scenario must retain hydration, extraction, replay, materialization, and idempotence evidence",
|
||||
));
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn has_pipeline_evidence(scenario: &crate::Token2022ValidationScenario) -> bool {
|
||||
let kinds = scenario
|
||||
.evidence
|
||||
.iter()
|
||||
.map(|evidence| return evidence.kind.as_str())
|
||||
.collect::<std::collections::BTreeSet<&str>>();
|
||||
if scenario.requires_canonical_hydration && !kinds.contains("canonical_hydration") {
|
||||
return false;
|
||||
}
|
||||
if scenario.requires_core_extraction && !kinds.contains("core_extraction") {
|
||||
return false;
|
||||
}
|
||||
if scenario.requires_decode_replay && !kinds.contains("decode_replay") {
|
||||
return false;
|
||||
}
|
||||
if scenario.requires_materialization && !kinds.contains("materialization") {
|
||||
return false;
|
||||
}
|
||||
if scenario.requires_second_replay && !kinds.contains("second_replay") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
fn scenario(status: crate::Token2022ValidationStatus) -> crate::Token2022ValidationScenario {
|
||||
return crate::Token2022ValidationScenario {
|
||||
id: "confidential_transfer_devnet".to_string(),
|
||||
environment: crate::Token2022ValidationEnvironment::Devnet,
|
||||
status,
|
||||
requires_canonical_hydration: true,
|
||||
requires_core_extraction: true,
|
||||
requires_decode_replay: true,
|
||||
requires_materialization: true,
|
||||
requires_second_replay: true,
|
||||
evidence: vec![
|
||||
crate::Token2022ValidationEvidence {
|
||||
kind: "signature".to_string(),
|
||||
value: "signature".to_string(),
|
||||
},
|
||||
crate::Token2022ValidationEvidence {
|
||||
kind: "canonical_hydration".to_string(),
|
||||
value: "1".to_string(),
|
||||
},
|
||||
crate::Token2022ValidationEvidence {
|
||||
kind: "core_extraction".to_string(),
|
||||
value: "1".to_string(),
|
||||
},
|
||||
crate::Token2022ValidationEvidence {
|
||||
kind: "decode_replay".to_string(),
|
||||
value: "1".to_string(),
|
||||
},
|
||||
crate::Token2022ValidationEvidence {
|
||||
kind: "materialization".to_string(),
|
||||
value: "1".to_string(),
|
||||
},
|
||||
crate::Token2022ValidationEvidence {
|
||||
kind: "second_replay".to_string(),
|
||||
value: "0_new_outputs".to_string(),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirmed_end_to_end_scenario_requires_complete_pipeline_evidence() {
|
||||
let report = crate::Token2022ValidationReport {
|
||||
scenarios: vec![scenario(crate::Token2022ValidationStatus::Confirmed)],
|
||||
};
|
||||
assert!(crate::validate_token_2022_validation_report(&report).is_ok());
|
||||
let mut incomplete = scenario(crate::Token2022ValidationStatus::Confirmed);
|
||||
incomplete.evidence.retain(|evidence| return evidence.kind != "second_replay");
|
||||
assert!(
|
||||
crate::validate_token_2022_validation_report(&crate::Token2022ValidationReport {
|
||||
scenarios: vec![incomplete]
|
||||
})
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn not_run_and_unavailable_scenarios_do_not_invent_evidence() {
|
||||
let mut not_run = scenario(crate::Token2022ValidationStatus::NotRun);
|
||||
not_run.evidence.clear();
|
||||
let mut unavailable = scenario(crate::Token2022ValidationStatus::Unavailable);
|
||||
unavailable.id = "permissioned_burn_devnet".to_string();
|
||||
unavailable.evidence.clear();
|
||||
assert!(
|
||||
crate::validate_token_2022_validation_report(&crate::Token2022ValidationReport {
|
||||
scenarios: vec![not_run, unavailable]
|
||||
})
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_ids_empty_evidence_and_completed_without_evidence_fail_closed() {
|
||||
let first = scenario(crate::Token2022ValidationStatus::Confirmed);
|
||||
let duplicate = first.clone();
|
||||
assert!(
|
||||
crate::validate_token_2022_validation_report(&crate::Token2022ValidationReport {
|
||||
scenarios: vec![first, duplicate]
|
||||
})
|
||||
.is_err()
|
||||
);
|
||||
let mut failed = scenario(crate::Token2022ValidationStatus::Failed);
|
||||
failed.evidence.clear();
|
||||
assert!(
|
||||
crate::validate_token_2022_validation_report(&crate::Token2022ValidationReport {
|
||||
scenarios: vec![failed]
|
||||
})
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_matrix_matches_compiled_inventory_and_observed_offline_evidence() {
|
||||
let matrix = crate::load_token_2022_validation_matrix();
|
||||
assert!(matrix.is_ok());
|
||||
let matrix = match matrix {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("unexpected matrix error: {error}"),
|
||||
};
|
||||
assert_eq!(matrix.scenarios.len(), 9);
|
||||
assert_eq!(matrix.scenarios[0].status, crate::Token2022ValidationStatus::Confirmed);
|
||||
assert_eq!(matrix.scenarios[0].evidence.len(), 4);
|
||||
assert!(
|
||||
matrix.scenarios[1..]
|
||||
.iter()
|
||||
.all(|scenario| return scenario.status == crate::Token2022ValidationStatus::NotRun)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirmed_matrix_scenario_requires_every_declared_evidence_kind() {
|
||||
let matrix = crate::load_token_2022_validation_matrix();
|
||||
assert!(matrix.is_ok());
|
||||
let mut matrix = match matrix {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("unexpected matrix error: {error}"),
|
||||
};
|
||||
matrix.scenarios[0].evidence.retain(|evidence| return evidence.kind != "clippy");
|
||||
assert!(crate::validate_token_2022_validation_matrix(&matrix).is_err());
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user