v0.1.0-pre.043
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# file: kb-pipeline/Cargo.toml
|
||||
# version: 12
|
||||
# version: 13
|
||||
|
||||
[package]
|
||||
name = "kb-pipeline"
|
||||
@@ -19,7 +19,6 @@ kb-lib = { path = "../kb-lib" }
|
||||
kb-onchain-transport = { path = "../kb-onchain-transport" }
|
||||
kb-program-ids = { path = "../kb-program-ids" }
|
||||
kb-store = { path = "../kb-store" }
|
||||
kb-wallet = { path = "../kb-wallet" }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
solana-address-lookup-table-interface.workspace = true
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-pipeline/src/lib.rs
|
||||
// version: 15
|
||||
// version: 16
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -12,36 +12,17 @@ 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_token2022_correlation;
|
||||
mod solana_token2022_crypto_preflight;
|
||||
mod solana_token2022_devnet_execution;
|
||||
mod solana_token2022_devnet_scenarios;
|
||||
mod solana_token2022_execution_orchestration;
|
||||
mod solana_token2022_preflight;
|
||||
mod solana_token2022_proof_orchestration;
|
||||
mod solana_token2022_stateful;
|
||||
mod solana_token2022_validation;
|
||||
mod solana_token_execution;
|
||||
mod solana_token_lifecycle;
|
||||
mod solana_token_stateful;
|
||||
|
||||
/// Emits one execution progress event for specialized pipeline orchestrators.
|
||||
pub(crate) use self::solana_execution::emit;
|
||||
/// Rejects one execution stage when the observer reports cancellation.
|
||||
pub(crate) use self::solana_execution::ensure_not_cancelled;
|
||||
/// Loads the persistent wallet configured by one execution profile.
|
||||
pub(crate) use self::solana_execution::load_profile_wallet;
|
||||
/// Formats one failed simulation without discarding runtime diagnostics.
|
||||
pub(crate) use self::solana_execution::simulation_failure_message;
|
||||
/// Formats safety violations for one denied execution plan.
|
||||
pub(crate) use self::solana_execution::violation_message;
|
||||
|
||||
/// Address category used by one targeted backfill campaign.
|
||||
pub use self::backfill::BackfillAddressKind;
|
||||
/// Chronological direction relative to one anchor signature.
|
||||
@@ -110,14 +91,6 @@ pub use self::decode_replay::new_decode_campaign_id;
|
||||
pub use self::plan::PipelineStage;
|
||||
/// Replay selection scope.
|
||||
pub use self::plan::ReplayScope;
|
||||
/// Complete request for one Devnet Associated Token Account execution.
|
||||
pub use self::solana_ata_execution::DevnetSplAssociatedTokenAccountExecutionRequest;
|
||||
/// Complete result of one Devnet Associated Token Account execution.
|
||||
pub use self::solana_ata_execution::DevnetSplAssociatedTokenAccountExecutionSummary;
|
||||
/// Executes one Devnet Associated Token Account simulation or authorized submission.
|
||||
pub use self::solana_ata_execution::execute_devnet_spl_associated_token_account;
|
||||
/// Simulates one Devnet Associated Token Account operation after stateful preflight.
|
||||
pub use self::solana_ata_execution::simulate_devnet_spl_associated_token_account;
|
||||
/// Stateful invariants observed after one confirmed Associated Token Account execution.
|
||||
pub use self::solana_ata_stateful::SplAssociatedTokenAccountPostExecutionReport;
|
||||
/// One machine-readable Associated Token Account stateful check.
|
||||
@@ -148,26 +121,6 @@ pub use self::solana_elgamal_registry_stateful::materialize_elgamal_registry_acc
|
||||
pub use self::solana_elgamal_registry_stateful::materialize_elgamal_registry_stateful_snapshot;
|
||||
/// Migrated read_elgamal_registry_stateful_snapshot contract.
|
||||
pub use self::solana_elgamal_registry_stateful::read_elgamal_registry_stateful_snapshot;
|
||||
/// Complete request for one bounded System Program transfer on Devnet.
|
||||
pub use self::solana_execution::DevnetSystemTransferRequest;
|
||||
/// Summary returned by one bounded System Program transfer execution.
|
||||
pub use self::solana_execution::DevnetSystemTransferSummary;
|
||||
/// No-op observer suitable for CLI tools and opt-in integration tests.
|
||||
pub use self::solana_execution::NoopSolanaExecutionObserver;
|
||||
/// Observer notified during Solana execution orchestration.
|
||||
pub use self::solana_execution::SolanaExecutionObserver;
|
||||
/// Progress event emitted by Solana execution orchestration.
|
||||
pub use self::solana_execution::SolanaExecutionProgressEvent;
|
||||
/// Severity of one Solana execution progress event.
|
||||
pub use self::solana_execution::SolanaExecutionProgressLevel;
|
||||
/// Executes one bounded System Program transfer on Devnet.
|
||||
pub use self::solana_execution::execute_devnet_system_transfer;
|
||||
/// Complete request for one SPL Memo v4 Devnet execution.
|
||||
pub use self::solana_memo_execution::DevnetMemoExecutionRequest;
|
||||
/// Complete result of one SPL Memo v4 Devnet execution and post-validation.
|
||||
pub use self::solana_memo_execution::DevnetMemoExecutionSummary;
|
||||
/// Executes one SPL Memo v4 Devnet simulation or explicitly authorized submission.
|
||||
pub use self::solana_memo_execution::execute_devnet_memo;
|
||||
/// One machine-readable native Solana stateful readiness check.
|
||||
pub use self::solana_stateful::SolanaCoreStatefulCheck;
|
||||
/// One contextual fact measured during native Solana stateful readiness.
|
||||
@@ -180,38 +133,6 @@ pub use self::solana_stateful::SolanaCoreStatefulReadinessRequest;
|
||||
pub use self::solana_stateful::SolanaCoreStatefulReadinessStatus;
|
||||
/// Inspects state required before simulating one native Solana operation.
|
||||
pub use self::solana_stateful::inspect_solana_core_stateful_readiness;
|
||||
/// Complete request for one Devnet classic SPL Token execution.
|
||||
pub use self::solana_token_execution::DevnetSplTokenExecutionRequest;
|
||||
/// Complete result of one Devnet classic SPL Token execution.
|
||||
pub use self::solana_token_execution::DevnetSplTokenExecutionSummary;
|
||||
/// Canonical hydration availability helper shared by Token lifecycle orchestration.
|
||||
pub(crate) use self::solana_token_execution::canonical_available;
|
||||
/// Decode completion helper shared by Token lifecycle orchestration.
|
||||
pub(crate) use self::solana_token_execution::decode_completed;
|
||||
/// Executes one Devnet classic SPL Token simulation or authorized submission.
|
||||
pub use self::solana_token_execution::execute_devnet_spl_token;
|
||||
/// Canonical signature hydration helper shared by Token lifecycle orchestration.
|
||||
pub(crate) use self::solana_token_execution::hydrate_signature;
|
||||
/// Targeted program replay helper shared by Token lifecycle orchestration.
|
||||
pub(crate) use self::solana_token_execution::replay_program;
|
||||
/// Simulates one Devnet classic SPL Token operation after stateful preflight.
|
||||
pub use self::solana_token_execution::simulate_devnet_spl_token;
|
||||
/// Complete request to prepare raw accounts for one Devnet SPL Token lifecycle.
|
||||
pub use self::solana_token_lifecycle::DevnetSplTokenLifecyclePreparationRequest;
|
||||
/// One prepared raw account step.
|
||||
pub use self::solana_token_lifecycle::DevnetSplTokenLifecyclePreparationStep;
|
||||
/// Summary of raw-account preparation for one lifecycle.
|
||||
pub use self::solana_token_lifecycle::DevnetSplTokenLifecyclePreparationSummary;
|
||||
/// Complete request for one controlled Devnet SPL Token lifecycle.
|
||||
pub use self::solana_token_lifecycle::DevnetSplTokenLifecycleRequest;
|
||||
/// Summary of one lifecycle operation step.
|
||||
pub use self::solana_token_lifecycle::DevnetSplTokenLifecycleStepSummary;
|
||||
/// Complete controlled lifecycle summary.
|
||||
pub use self::solana_token_lifecycle::DevnetSplTokenLifecycleSummary;
|
||||
/// Executes one controlled Devnet SPL Token lifecycle.
|
||||
pub use self::solana_token_lifecycle::execute_devnet_spl_token_lifecycle;
|
||||
/// Prepares raw accounts for one controlled Devnet SPL Token lifecycle.
|
||||
pub use self::solana_token_lifecycle::prepare_devnet_spl_token_lifecycle_accounts;
|
||||
/// One machine-readable classic SPL Token stateful check.
|
||||
pub use self::solana_token_stateful::SplTokenStatefulCheck;
|
||||
/// One contextual classic SPL Token stateful fact.
|
||||
@@ -244,22 +165,6 @@ pub use self::solana_token2022_crypto_preflight::Token2022ProofContextRequiremen
|
||||
pub use self::solana_token2022_crypto_preflight::ZK_PROOF_CONTEXT_META_BYTES;
|
||||
/// Inspects bounded Token-2022 cryptographic proof contexts.
|
||||
pub use self::solana_token2022_crypto_preflight::inspect_token2022_cryptographic_preflight;
|
||||
/// Complete request for one Devnet Token-2022 execution.
|
||||
pub use self::solana_token2022_devnet_execution::DevnetSplToken2022ExecutionRequest;
|
||||
/// Complete result of one Devnet Token-2022 execution.
|
||||
pub use self::solana_token2022_devnet_execution::DevnetSplToken2022ExecutionSummary;
|
||||
/// Executes one Devnet Token-2022 simulation or authorized submission.
|
||||
pub use self::solana_token2022_devnet_execution::execute_devnet_spl_token2022;
|
||||
/// Simulates one Devnet Token-2022 operation after stateful preflight.
|
||||
pub use self::solana_token2022_devnet_execution::simulate_devnet_spl_token2022;
|
||||
/// Stable category of one independent Devnet validation scenario.
|
||||
pub use self::solana_token2022_devnet_scenarios::DevnetSplValidationFamily;
|
||||
/// Current implementation status of one Devnet validation scenario.
|
||||
pub use self::solana_token2022_devnet_scenarios::DevnetSplValidationImplementationStatus;
|
||||
/// One independent Devnet validation scenario exposed to applications.
|
||||
pub use self::solana_token2022_devnet_scenarios::DevnetSplValidationScenario;
|
||||
/// Returns the complete ordered Devnet scenario inventory.
|
||||
pub use self::solana_token2022_devnet_scenarios::devnet_spl_validation_scenarios;
|
||||
/// Maximum number of distinct signers accepted by one Token-2022 execution envelope.
|
||||
pub use self::solana_token2022_execution_orchestration::MAX_TOKEN2022_EXECUTION_SIGNERS;
|
||||
/// One explicit stateful postcondition retained after Token-2022 execution.
|
||||
@@ -316,28 +221,6 @@ pub use self::solana_token2022_stateful::materialize_token2022_account_info_resu
|
||||
pub use self::solana_token2022_stateful::materialize_token2022_stateful_snapshot;
|
||||
/// Migrated read_token2022_stateful_snapshot contract.
|
||||
pub use self::solana_token2022_stateful::read_token2022_stateful_snapshot;
|
||||
/// Maximum number of evidence records accepted in one validation report.
|
||||
pub use self::solana_token2022_validation::MAX_TOKEN2022_VALIDATION_EVIDENCE;
|
||||
/// Required validation environment for one Token-2022 scenario.
|
||||
pub use self::solana_token2022_validation::Token2022ValidationEnvironment;
|
||||
/// One bounded proof attached to a Token-2022 validation scenario.
|
||||
pub use self::solana_token2022_validation::Token2022ValidationEvidence;
|
||||
/// Machine-readable Token-2022 validation matrix.
|
||||
pub use self::solana_token2022_validation::Token2022ValidationMatrix;
|
||||
/// One scenario declared by the canonical validation matrix.
|
||||
pub use self::solana_token2022_validation::Token2022ValidationMatrixScenario;
|
||||
/// Complete bounded Token-2022 validation report.
|
||||
pub use self::solana_token2022_validation::Token2022ValidationReport;
|
||||
/// One declared Token-2022 validation scenario and its observed evidence.
|
||||
pub use self::solana_token2022_validation::Token2022ValidationScenario;
|
||||
/// Exact status of one Token-2022 validation scenario.
|
||||
pub use self::solana_token2022_validation::Token2022ValidationStatus;
|
||||
/// Loads and validates the canonical Token-2022 validation matrix.
|
||||
pub use self::solana_token2022_validation::load_token2022_validation_matrix;
|
||||
/// Validates one Token-2022 validation matrix.
|
||||
pub use self::solana_token2022_validation::validate_token2022_validation_matrix;
|
||||
/// Validates one bounded Token-2022 milestone report.
|
||||
pub use self::solana_token2022_validation::validate_token2022_validation_report;
|
||||
|
||||
/// Canonical tracing target for pipeline orchestration.
|
||||
pub(crate) use self::constants::TRACING_TARGET;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,903 +0,0 @@
|
||||
// file: kb-pipeline/src/solana_memo_execution.rs
|
||||
// version: 1
|
||||
|
||||
//! 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_lib::EX_SPL_MEMO_MAX_MESSAGE_BYTES {
|
||||
return std::result::Result::Err(kb_core::Error::config(format!(
|
||||
"Devnet Memo payload length {} exceeds the executor limit {}",
|
||||
self.message.len(),
|
||||
kb_lib::EX_SPL_MEMO_MAX_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_lib::ExApiExecutionCluster,
|
||||
/// 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_lib::ExApiPreparedExecutionPlan,
|
||||
/// Recent blockhash used by the exact transaction.
|
||||
pub latest_blockhash: kb_onchain_transport::LatestBlockhashResult,
|
||||
/// Fee estimate for the exact compiled message.
|
||||
pub fee: kb_onchain_transport::FeeForMessageResult,
|
||||
/// Exact simulation result bound to the compiled message.
|
||||
pub simulation: kb_lib::ExApiExecutionSimulationResult,
|
||||
/// Submission result when explicitly authorized.
|
||||
pub send_result: std::option::Option<kb_lib::ExApiExecutionSendResult>,
|
||||
/// Confirmation result when submitted.
|
||||
pub confirmation: std::option::Option<kb_lib::ExApiExecutionConfirmationResult>,
|
||||
/// 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::MaterializedEventQueryRow>,
|
||||
/// Aggregated post-execution validation diagnostic.
|
||||
pub post_execution: std::option::Option<kb_lib::ExApiPostExecutionDiagnostic>,
|
||||
}
|
||||
|
||||
/// 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_onchain_transport::HttpEndpointPool,
|
||||
store: &S,
|
||||
profile: &kb_config::ProfileConfig,
|
||||
workspace_root: &std::path::Path,
|
||||
request: &crate::DevnetMemoExecutionRequest,
|
||||
decoders: &[std::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>],
|
||||
materializers: &[std::sync::Arc<dyn kb_lib::MtApiEventMaterializer>],
|
||||
observer: &O,
|
||||
) -> kb_core::Result<crate::DevnetMemoExecutionSummary>
|
||||
where
|
||||
S: kb_store::RawTransactionStore
|
||||
+ kb_store::CoreExtractionStore
|
||||
+ kb_store::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) = validate_profile(profile, request) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
if let std::result::Result::Err(error) = crate::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_lib::ExApiExecutionCluster::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::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_lib::MdPubkey(wallet_summary.public_key.clone());
|
||||
let balance = match http_pool
|
||||
.get_balance_for_role(
|
||||
request.query_role.as_str(),
|
||||
&fee_payer,
|
||||
&kb_onchain_transport::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 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_lib::ExSafetyChecker.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_lib::ExSafetyDecision::Deny {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_plan_denied",
|
||||
crate::violation_message(plan_evaluation.violations.as_slice()),
|
||||
));
|
||||
}
|
||||
let latest_blockhash = match http_pool
|
||||
.get_latest_blockhash_for_role(
|
||||
request.query_role.as_str(),
|
||||
&kb_onchain_transport::GetLatestBlockhashConfig::confirmed(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let unsigned = match kb_lib::executor_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_onchain_transport::GetFeeForMessageConfig::new(
|
||||
kb_onchain_transport::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_onchain_transport::SimulateTransactionConfig::new(
|
||||
kb_onchain_transport::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::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_lib::ExApiExecutionCluster::Devnet,
|
||||
kb_lib::ExApiExecutionBlockhashKind::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_lib::ExApiExecutionCluster::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::simulation_failure_message(&summary.simulation),
|
||||
));
|
||||
}
|
||||
let send_evaluation =
|
||||
match kb_lib::ExSafetyChecker.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_lib::ExSafetyDecision::Deny {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_send_denied",
|
||||
crate::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_lib::ExApiPostExecutionDiagnostic {
|
||||
signature: signature.clone(),
|
||||
canonical_inserted: false,
|
||||
core_extracted: false,
|
||||
decode_replayed: false,
|
||||
materialized: false,
|
||||
diagnostics: std::vec::Vec::new(),
|
||||
};
|
||||
let send_config = match kb_onchain_transport::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_lib::ExApiExecutionCluster::Devnet));
|
||||
let confirmation_config =
|
||||
match kb_onchain_transport::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_lib::ExApiExecutionCluster::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_lib::ExApiExecutionConfirmationStatus::Confirmed
|
||||
| kb_lib::ExApiExecutionConfirmationStatus::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 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 = 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 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 = decode_completed(&first_replay);
|
||||
summary.decode_replay = std::option::Option::Some(first_replay);
|
||||
let filter = match kb_store::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::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 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_lib::MdPubkey,
|
||||
) -> kb_core::Result<kb_lib::ExApiPreparedExecutionPlan> {
|
||||
let signers = if request.include_wallet_as_memo_signer {
|
||||
std::vec![kb_lib::ExSplMemoSigner { pubkey: fee_payer.clone() }]
|
||||
} else {
|
||||
std::vec::Vec::new()
|
||||
};
|
||||
let intent = kb_lib::ExSplMemoExecutionIntent {
|
||||
intent_id: request.intent_id.clone(),
|
||||
fee_payer: fee_payer.clone(),
|
||||
policy: kb_lib::ExApiExecutionPolicy {
|
||||
cluster: kb_lib::ExApiExecutionClusterPolicy {
|
||||
expected_cluster: kb_lib::ExApiExecutionCluster::Devnet,
|
||||
allow_mainnet: false,
|
||||
mainnet_confirmation: false,
|
||||
},
|
||||
simulation: kb_lib::ExApiExecutionSimulationPolicy::Required,
|
||||
blockhash: kb_lib::ExApiExecutionBlockhashPolicy {
|
||||
kind: kb_lib::ExApiExecutionBlockhashKind::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_lib::ExApiExecutionCostLimit {
|
||||
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_lib::ExApiPostExecutionValidationPolicy {
|
||||
canonical_insert_required: true,
|
||||
core_extraction_required: true,
|
||||
decode_replay_required: true,
|
||||
materialization_required: true,
|
||||
},
|
||||
},
|
||||
operation: kb_lib::ExSplMemoOperation::AddMemo {
|
||||
generation: kb_lib::ExSplMemoGeneration::V4,
|
||||
message: request.message.clone(),
|
||||
signers,
|
||||
},
|
||||
};
|
||||
return kb_lib::ExApiTypedInstructionExecutor::build_prepared_plan(
|
||||
&kb_lib::ExSplMemoExecutor,
|
||||
&intent,
|
||||
);
|
||||
}
|
||||
|
||||
async fn hydrate_signature<S, O>(
|
||||
http_pool: &kb_onchain_transport::HttpEndpointPool,
|
||||
store: &S,
|
||||
profile: &kb_config::ProfileConfig,
|
||||
request: &crate::DevnetMemoExecutionRequest,
|
||||
observer: &O,
|
||||
signature: &kb_lib::MdSignature,
|
||||
) -> kb_core::Result<crate::BackfillSummary>
|
||||
where
|
||||
S: kb_store::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 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_lib::MdSignature,
|
||||
include_materialized_state: bool,
|
||||
decoders: &[std::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>],
|
||||
materializers: &[std::sync::Arc<dyn kb_lib::MtApiEventMaterializer>],
|
||||
observer: &O,
|
||||
) -> kb_core::Result<crate::DecodeReplaySummary>
|
||||
where
|
||||
S: kb_store::DecodePipelineStore + Sync,
|
||||
O: crate::SolanaExecutionObserver,
|
||||
{
|
||||
let mut states = std::vec![
|
||||
kb_store::CoreInstructionProcessingState::Pending,
|
||||
kb_store::CoreInstructionProcessingState::Failed,
|
||||
kb_store::CoreInstructionProcessingState::ReplayRequested,
|
||||
];
|
||||
if include_materialized_state {
|
||||
states.push(kb_store::CoreInstructionProcessingState::Materialized);
|
||||
}
|
||||
let selection = match kb_store::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_lib::EX_SPL_MEMO_MAX_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 = local_devnet_profile();
|
||||
let fee_payer = kb_lib::MdPubkey(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 super::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_lib::ExSafetyChecker.evaluate_prepared_plan(&plan).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn submission_requires_profile_enablement_and_confirmation() {
|
||||
let profile = local_devnet_profile();
|
||||
let mut request = crate::DevnetMemoExecutionRequest::new("memo-4", "hello");
|
||||
assert!(super::validate_profile(&profile, &request).is_ok());
|
||||
request.submit = true;
|
||||
assert!(super::validate_profile(&profile, &request).is_err());
|
||||
request.operator_confirmed = true;
|
||||
assert!(super::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 = 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_onchain_transport::HttpEndpointPool::from_profile(&profile) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("HTTP pool creation failed: {error}"),
|
||||
};
|
||||
let store_options = match kb_store::PostgresStoreOptions::new(
|
||||
profile.database.postgres.url.clone(),
|
||||
profile.database.postgres.max_connections,
|
||||
profile.database.postgres.connect_timeout_ms,
|
||||
false,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("PostgreSQL options failed: {error}"),
|
||||
};
|
||||
let store = match kb_store::PostgresStore::connect(store_options).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-bot3 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_lib::DcApiInstructionDecoder>> =
|
||||
std::vec![std::sync::Arc::new(kb_lib::DcSplMemoDecoder)];
|
||||
let materializers: std::vec::Vec<std::sync::Arc<dyn kb_lib::MtApiEventMaterializer>> =
|
||||
std::vec![std::sync::Arc::new(kb_lib::MtTransactionAnnotationMaterializer,)];
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,921 +0,0 @@
|
||||
// file: kb-pipeline/src/solana_token2022_devnet_execution.rs
|
||||
// version: 1
|
||||
|
||||
//! 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_lib::ExSplToken2022Operation,
|
||||
/// 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::DevnetSplToken2022ExecutionRequest {
|
||||
/// Creates a conservative simulation-only request.
|
||||
pub fn new(
|
||||
intent_id: impl std::convert::Into<std::string::String>,
|
||||
operation: kb_lib::ExSplToken2022Operation,
|
||||
) -> 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_lib::ExApiExecutionCluster,
|
||||
/// 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_lib::ExApiPreparedExecutionPlan,
|
||||
/// Recent blockhash used by the exact transaction.
|
||||
pub latest_blockhash: kb_onchain_transport::LatestBlockhashResult,
|
||||
/// Fee estimate for the exact compiled message.
|
||||
pub fee: kb_onchain_transport::FeeForMessageResult,
|
||||
/// Exact simulation result bound to the compiled message.
|
||||
pub simulation: kb_lib::ExApiExecutionSimulationResult,
|
||||
/// Submission result when explicitly authorized.
|
||||
pub send_result: std::option::Option<kb_lib::ExApiExecutionSendResult>,
|
||||
/// Confirmation result when submitted.
|
||||
pub confirmation: std::option::Option<kb_lib::ExApiExecutionConfirmationResult>,
|
||||
/// 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::MaterializedEventQueryRow>,
|
||||
/// Aggregated post-execution validation diagnostic.
|
||||
pub post_execution: std::option::Option<kb_lib::ExApiPostExecutionDiagnostic>,
|
||||
}
|
||||
|
||||
struct PreparedToken2022Execution {
|
||||
wallet: kb_wallet::TemporaryWallet,
|
||||
unsigned: kb_lib::ExSolanaUnsignedTransaction,
|
||||
evidence: kb_lib::ExSolanaSimulationEvidence,
|
||||
summary: crate::DevnetSplToken2022ExecutionSummary,
|
||||
}
|
||||
|
||||
/// Simulates one Devnet Token-2022 operation after stateful preflight.
|
||||
pub async fn simulate_devnet_spl_token2022<O>(
|
||||
http_pool: &kb_onchain_transport::HttpEndpointPool,
|
||||
profile: &kb_config::ProfileConfig,
|
||||
workspace_root: &std::path::Path,
|
||||
request: &crate::DevnetSplToken2022ExecutionRequest,
|
||||
observer: &O,
|
||||
) -> kb_core::Result<crate::DevnetSplToken2022ExecutionSummary>
|
||||
where
|
||||
O: crate::SolanaExecutionObserver,
|
||||
{
|
||||
if request.submit {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"simulate_devnet_spl_token2022 requires submit=false",
|
||||
));
|
||||
}
|
||||
let prepared =
|
||||
match 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_token2022<S, O>(
|
||||
http_pool: &kb_onchain_transport::HttpEndpointPool,
|
||||
store: &S,
|
||||
profile: &kb_config::ProfileConfig,
|
||||
workspace_root: &std::path::Path,
|
||||
request: &crate::DevnetSplToken2022ExecutionRequest,
|
||||
decoders: &[std::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>],
|
||||
materializers: &[std::sync::Arc<dyn kb_lib::MtApiEventMaterializer>],
|
||||
observer: &O,
|
||||
) -> kb_core::Result<crate::DevnetSplToken2022ExecutionSummary>
|
||||
where
|
||||
S: kb_store::RawTransactionStore
|
||||
+ kb_store::CoreExtractionStore
|
||||
+ kb_store::DecodePipelineStore
|
||||
+ Sync,
|
||||
O: crate::SolanaExecutionObserver,
|
||||
{
|
||||
let prepared =
|
||||
match 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::simulation_failure_message(&prepared.summary.simulation),
|
||||
));
|
||||
}
|
||||
if let std::result::Result::Err(error) = 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_lib::ExSafetyChecker
|
||||
.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_lib::ExSafetyDecision::Deny {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_send_denied",
|
||||
crate::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_lib::ExApiPostExecutionDiagnostic {
|
||||
signature: signature.clone(),
|
||||
canonical_inserted: false,
|
||||
core_extracted: false,
|
||||
decode_replayed: false,
|
||||
materialized: false,
|
||||
diagnostics: std::vec::Vec::new(),
|
||||
};
|
||||
let send_config = match kb_onchain_transport::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_lib::ExApiExecutionCluster::Devnet));
|
||||
let confirmation_config =
|
||||
match kb_onchain_transport::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_lib::ExApiExecutionCluster::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_lib::ExApiExecutionConfirmationStatus::Confirmed
|
||||
| kb_lib::ExApiExecutionConfirmationStatus::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::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::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::replay_program(
|
||||
store,
|
||||
&signature,
|
||||
false,
|
||||
request.force_post_validation_replay,
|
||||
&[kb_program_ids::SPL_TOKEN2022_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::decode_completed(&first_replay);
|
||||
summary.decode_replay = std::option::Option::Some(first_replay);
|
||||
let filter = match kb_store::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::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_token2022")
|
||||
.collect();
|
||||
diagnostic.materialized =
|
||||
!summary.plan.policy.post_execution_validation.materialization_required
|
||||
|| !summary.materializations.is_empty();
|
||||
let second_replay = match crate::replay_program(
|
||||
store,
|
||||
&signature,
|
||||
true,
|
||||
request.force_post_validation_replay,
|
||||
&[kb_program_ids::SPL_TOKEN2022_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_onchain_transport::HttpEndpointPool,
|
||||
profile: &kb_config::ProfileConfig,
|
||||
workspace_root: &std::path::Path,
|
||||
request: &crate::DevnetSplToken2022ExecutionRequest,
|
||||
observer: &O,
|
||||
) -> kb_core::Result<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) = validate_profile(profile, request) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
if let std::result::Result::Err(error) = crate::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_lib::ExApiExecutionCluster::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 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_token2022_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::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_lib::MdPubkey(wallet_summary.public_key.clone());
|
||||
let balance = match http_pool
|
||||
.get_balance_for_role(
|
||||
request.query_role.as_str(),
|
||||
&fee_payer,
|
||||
&kb_onchain_transport::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 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_lib::ExSafetyChecker.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_lib::ExSafetyDecision::Deny {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_plan_denied",
|
||||
crate::violation_message(plan_evaluation.violations.as_slice()),
|
||||
));
|
||||
}
|
||||
let latest_blockhash = match http_pool
|
||||
.get_latest_blockhash_for_role(
|
||||
request.query_role.as_str(),
|
||||
&kb_onchain_transport::GetLatestBlockhashConfig::confirmed(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let unsigned = match kb_lib::executor_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_onchain_transport::GetFeeForMessageConfig::new(
|
||||
kb_onchain_transport::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_onchain_transport::SimulateTransactionConfig::new(
|
||||
kb_onchain_transport::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::emit(
|
||||
observer,
|
||||
crate::SolanaExecutionProgressLevel::Info,
|
||||
"spl_token2022_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_lib::ExApiExecutionCluster::Devnet,
|
||||
kb_lib::ExApiExecutionBlockhashKind::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::DevnetSplToken2022ExecutionSummary {
|
||||
profile_name: profile.name.clone(),
|
||||
cluster: kb_lib::ExApiExecutionCluster::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(PreparedToken2022Execution {
|
||||
wallet,
|
||||
unsigned,
|
||||
evidence,
|
||||
summary,
|
||||
});
|
||||
}
|
||||
|
||||
fn preflight_request(
|
||||
request: &crate::DevnetSplToken2022ExecutionRequest,
|
||||
) -> kb_core::Result<crate::Token2022PreflightRequest> {
|
||||
let operation = match &request.operation {
|
||||
kb_lib::ExSplToken2022Operation::Instruction { value } => value.as_ref(),
|
||||
kb_lib::ExSplToken2022Operation::Batch { instructions: _ } => {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_spl_token2022_batch_not_supported",
|
||||
"Devnet Token-2022 validation scenarios require one non-batch operation",
|
||||
));
|
||||
},
|
||||
};
|
||||
let mut requirements = std::vec::Vec::new();
|
||||
match operation {
|
||||
kb_lib::ExSplTokenSingleOperation::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_lib::ExSplTokenSingleOperation::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_lib::ExSplTokenSingleOperation::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_lib::ExSplTokenSingleOperation::Revoke { source, authority } => {
|
||||
requirements.push(account_requirement(
|
||||
"source",
|
||||
source,
|
||||
&kb_lib::MdPubkey(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_lib::ExSplTokenSingleOperation::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_lib::ExSplTokenSingleOperation::FreezeAccount { account, mint, authority }
|
||||
| kb_lib::ExSplTokenSingleOperation::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_lib::ExSplTokenSingleOperation::CloseAccount { account, destination: _, authority } => {
|
||||
requirements.push(account_requirement(
|
||||
"account",
|
||||
account,
|
||||
&kb_lib::MdPubkey(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_token2022_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_TOKEN2022_PREFLIGHT_ACCOUNTS,
|
||||
max_total_data_bytes: crate::MAX_TOKEN2022_PREFLIGHT_TOTAL_BYTES,
|
||||
requirements,
|
||||
elgamal_registry: std::option::Option::None,
|
||||
});
|
||||
}
|
||||
|
||||
fn validate_single_authority(authority: &kb_lib::ExSplTokenAuthority) -> kb_core::Result<()> {
|
||||
if !authority.multisig_signers.is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_spl_token2022_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_lib::MdPubkey,
|
||||
decimals: std::option::Option<u8>,
|
||||
) -> crate::Token2022PreflightRequirement {
|
||||
return crate::Token2022PreflightRequirement {
|
||||
role: role.to_string(),
|
||||
account: mint.clone(),
|
||||
kind: kb_lib::DcToken2022StateKind::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_lib::MdPubkey,
|
||||
mint: &kb_lib::MdPubkey,
|
||||
owner: std::option::Option<kb_lib::MdPubkey>,
|
||||
) -> crate::Token2022PreflightRequirement {
|
||||
return crate::Token2022PreflightRequirement {
|
||||
role: role.to_string(),
|
||||
account: account.clone(),
|
||||
kind: kb_lib::DcToken2022StateKind::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::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::DevnetSplToken2022ExecutionRequest,
|
||||
fee_payer: kb_lib::MdPubkey,
|
||||
) -> kb_core::Result<kb_lib::ExApiPreparedExecutionPlan> {
|
||||
let materialization_required = operation_requires_materialization(&request.operation);
|
||||
let authorized_signers = std::vec![fee_payer.clone()];
|
||||
let intent = kb_lib::ExSplToken2022ExecutionIntent {
|
||||
intent_id: request.intent_id.clone(),
|
||||
fee_payer,
|
||||
policy: kb_lib::ExApiExecutionPolicy {
|
||||
cluster: kb_lib::ExApiExecutionClusterPolicy {
|
||||
expected_cluster: kb_lib::ExApiExecutionCluster::Devnet,
|
||||
allow_mainnet: false,
|
||||
mainnet_confirmation: false,
|
||||
},
|
||||
simulation: kb_lib::ExApiExecutionSimulationPolicy::Required,
|
||||
blockhash: kb_lib::ExApiExecutionBlockhashPolicy {
|
||||
kind: kb_lib::ExApiExecutionBlockhashKind::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_lib::ExApiExecutionCostLimit {
|
||||
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_lib::ExApiPostExecutionValidationPolicy {
|
||||
canonical_insert_required: true,
|
||||
core_extraction_required: true,
|
||||
decode_replay_required: true,
|
||||
materialization_required,
|
||||
},
|
||||
},
|
||||
operation: request.operation.clone(),
|
||||
};
|
||||
return kb_lib::ExApiTypedInstructionExecutor::build_prepared_plan(
|
||||
&kb_lib::ExSplToken2022Executor,
|
||||
&intent,
|
||||
);
|
||||
}
|
||||
|
||||
fn operation_requires_materialization(_operation: &kb_lib::ExSplToken2022Operation) -> 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_token2022_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(());
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
// file: kb-pipeline/src/solana_token2022_devnet_scenarios.rs
|
||||
// version: 4
|
||||
|
||||
//! 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("token2022_mint_to_checked", "Token-2022 MintToChecked", kb_lib::EX_SPL_TOKEN2022_MINT_TO_CHECKED_OPERATION, &["TOKEN2022_MINT", "TOKEN2022_SOURCE", "TOKEN2022_AUTHORITY", "TOKEN2022_DECIMALS", "TOKEN2022_MINT_AMOUNT_RAW"]),
|
||||
public_scenario("token2022_transfer_checked", "Token-2022 TransferChecked", kb_lib::EX_SPL_TOKEN2022_TRANSFER_CHECKED_OPERATION, &["TOKEN2022_SOURCE", "TOKEN2022_MINT", "TOKEN2022_DESTINATION", "TOKEN2022_AUTHORITY", "TOKEN2022_DECIMALS", "TOKEN2022_TRANSFER_AMOUNT_RAW"]),
|
||||
public_scenario("token2022_approve_checked", "Token-2022 ApproveChecked", kb_lib::EX_SPL_TOKEN2022_APPROVE_CHECKED_OPERATION, &["TOKEN2022_SOURCE", "TOKEN2022_MINT", "TOKEN2022_DELEGATE", "TOKEN2022_AUTHORITY", "TOKEN2022_DECIMALS", "TOKEN2022_APPROVE_AMOUNT_RAW"]),
|
||||
public_scenario("token2022_revoke", "Token-2022 Revoke", kb_lib::EX_SPL_TOKEN2022_REVOKE_OPERATION, &["TOKEN2022_SOURCE", "TOKEN2022_AUTHORITY"]),
|
||||
public_scenario("token2022_burn_checked", "Token-2022 BurnChecked", kb_lib::EX_SPL_TOKEN2022_BURN_CHECKED_OPERATION, &["TOKEN2022_SOURCE", "TOKEN2022_MINT", "TOKEN2022_AUTHORITY", "TOKEN2022_DECIMALS", "TOKEN2022_BURN_AMOUNT_RAW"]),
|
||||
public_scenario("token2022_freeze_account", "Token-2022 FreezeAccount", kb_lib::EX_SPL_TOKEN2022_FREEZE_ACCOUNT_OPERATION, &["TOKEN2022_SOURCE", "TOKEN2022_MINT", "TOKEN2022_FREEZE_AUTHORITY"]),
|
||||
public_scenario("token2022_thaw_account", "Token-2022 ThawAccount", kb_lib::EX_SPL_TOKEN2022_THAW_ACCOUNT_OPERATION, &["TOKEN2022_SOURCE", "TOKEN2022_MINT", "TOKEN2022_FREEZE_AUTHORITY"]),
|
||||
public_scenario("token2022_close_destination", "Token-2022 CloseAccount destination", kb_lib::EX_SPL_TOKEN2022_CLOSE_ACCOUNT_OPERATION, &["TOKEN2022_DESTINATION", "KB_DEVNET_WALLET_ADDRESS", "TOKEN2022_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("token2022_configure_confidential_account", "Token-2022 ConfigureAccountWithRegistry", kb_lib::EX_SPL_TOKEN2022_CONFIGURE_CONFIDENTIAL_TRANSFER_ACCOUNT_WITH_REGISTRY_OPERATION, &["TOKEN2022_SOURCE", "TOKEN2022_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;
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -1,440 +0,0 @@
|
||||
// file: kb-pipeline/src/solana_token2022_validation.rs
|
||||
// version: 4
|
||||
|
||||
//! Machine-readable validation evidence contract for the Token-2022 milestone.
|
||||
|
||||
/// Maximum number of evidence records accepted in one validation report.
|
||||
pub const MAX_TOKEN2022_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_token2022_validation_matrix() -> kb_core::Result<crate::Token2022ValidationMatrix> {
|
||||
let parsed = match serde_json::from_str::<crate::Token2022ValidationMatrix>(include_str!(
|
||||
"../../docs/SPL_TOKEN2022_VALIDATION_MATRIX.json"
|
||||
)) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token2022_validation_matrix_invalid_json",
|
||||
format!("Token-2022 validation matrix JSON is invalid: {error}"),
|
||||
));
|
||||
},
|
||||
};
|
||||
if let std::result::Result::Err(error) = validate_token2022_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_token2022_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(
|
||||
"token2022_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(
|
||||
"token2022_validation_matrix_status_vocabulary_mismatch",
|
||||
"Token-2022 validation status vocabulary differs from the compiled contract",
|
||||
));
|
||||
}
|
||||
let expected_ids = [
|
||||
"offline_full_regression",
|
||||
"token2022_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(
|
||||
"token2022_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_TOKEN2022_VALIDATION_EVIDENCE
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token2022_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(
|
||||
"token2022_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(
|
||||
"token2022_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(
|
||||
"token2022_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_token2022_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(
|
||||
"token2022_validation_scenario_identity_invalid",
|
||||
"Token-2022 validation scenario ids must be non-empty and unique",
|
||||
));
|
||||
}
|
||||
if scenario.evidence.len() > crate::MAX_TOKEN2022_VALIDATION_EVIDENCE {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token2022_validation_evidence_limit_exceeded",
|
||||
format!(
|
||||
"Token-2022 validation accepts at most {} evidence records per scenario",
|
||||
crate::MAX_TOKEN2022_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(
|
||||
"token2022_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(
|
||||
"token2022_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(
|
||||
"token2022_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_token2022_validation_report(&report).is_ok());
|
||||
let mut incomplete = scenario(crate::Token2022ValidationStatus::Confirmed);
|
||||
incomplete.evidence.retain(|evidence| return evidence.kind != "second_replay");
|
||||
assert!(
|
||||
crate::validate_token2022_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_token2022_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_token2022_validation_report(&crate::Token2022ValidationReport {
|
||||
scenarios: vec![first, duplicate]
|
||||
})
|
||||
.is_err()
|
||||
);
|
||||
let mut failed = scenario(crate::Token2022ValidationStatus::Failed);
|
||||
failed.evidence.clear();
|
||||
assert!(
|
||||
crate::validate_token2022_validation_report(&crate::Token2022ValidationReport {
|
||||
scenarios: vec![failed]
|
||||
})
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_matrix_matches_compiled_inventory_and_observed_offline_evidence() {
|
||||
let matrix = crate::load_token2022_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_token2022_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_token2022_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
Reference in New Issue
Block a user