v0.1.0-pre.016
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// file: kb-lib/src/executor.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
//! Consolidated executor modules.
|
||||
|
||||
@@ -22,6 +22,7 @@ mod orderbook;
|
||||
mod perpetuals;
|
||||
mod router;
|
||||
mod rwa;
|
||||
mod safety;
|
||||
mod solana;
|
||||
mod spl;
|
||||
mod stable;
|
||||
@@ -236,6 +237,14 @@ pub use self::router::ExRouterOkxLabsV1Executor;
|
||||
pub use self::router::ExRouterOkxLabsV2Executor;
|
||||
/// Exposes the reserved `rwa/ondo_global_markets` executor.
|
||||
pub use self::rwa::ExRwaOndoGlobalMarketsExecutor;
|
||||
/// Exposes the stateless execution safety checker.
|
||||
pub use self::safety::ExSafetyChecker;
|
||||
/// Exposes an execution safety decision.
|
||||
pub use self::safety::ExSafetyDecision;
|
||||
/// Exposes a complete execution safety evaluation.
|
||||
pub use self::safety::ExSafetyEvaluation;
|
||||
/// Exposes one execution safety violation.
|
||||
pub use self::safety::ExSafetyViolation;
|
||||
/// Exposes the Address Lookup Table close operation code.
|
||||
pub use self::solana::EX_SOLANA_CORE_ADDRESS_LOOKUP_TABLE_CLOSE_OPERATION;
|
||||
/// Exposes the Address Lookup Table create operation code.
|
||||
@@ -484,14 +493,28 @@ pub use self::solana::ExSolanaCoreVoteLockout;
|
||||
pub use self::solana::ExSolanaCoreZkElGamalContextState;
|
||||
/// Exposes the official ZK ElGamal proof kinds.
|
||||
pub use self::solana::ExSolanaCoreZkElGamalProofType;
|
||||
/// Exposes the stable SPL Memo add-memo operation code.
|
||||
pub use self::spl::EX_SPL_MEMO_ADD_MEMO_OPERATION;
|
||||
/// Exposes the SPL Memo payload bound.
|
||||
pub use self::spl::EX_SPL_MEMO_MAX_MESSAGE_BYTES;
|
||||
/// Exposes the SPL Memo signer bound.
|
||||
pub use self::spl::EX_SPL_MEMO_MAX_SIGNERS;
|
||||
/// Exposes the reserved `spl/account_compression` executor.
|
||||
pub use self::spl::ExSplAccountCompressionExecutor;
|
||||
/// Exposes the reserved `spl/associated_token_account` executor.
|
||||
pub use self::spl::ExSplAssociatedTokenAccountExecutor;
|
||||
/// Exposes the reserved `spl/elgamal_registry` executor.
|
||||
pub use self::spl::ExSplElgamalRegistryExecutor;
|
||||
/// Exposes the reserved `spl/memo` executor.
|
||||
/// Exposes the typed SPL Memo execution intent.
|
||||
pub use self::spl::ExSplMemoExecutionIntent;
|
||||
/// Exposes the SPL Memo executor.
|
||||
pub use self::spl::ExSplMemoExecutor;
|
||||
/// Exposes the exact SPL Memo generation.
|
||||
pub use self::spl::ExSplMemoGeneration;
|
||||
/// Exposes the typed SPL Memo operation.
|
||||
pub use self::spl::ExSplMemoOperation;
|
||||
/// Exposes one ordered SPL Memo signer.
|
||||
pub use self::spl::ExSplMemoSigner;
|
||||
/// Exposes the reserved `spl/noop` executor.
|
||||
pub use self::spl::ExSplNoopExecutor;
|
||||
/// Exposes the reserved `spl/single_pool` executor.
|
||||
@@ -575,12 +598,16 @@ pub(crate) use self::solana::executor_solana_core_validate_seeded_address;
|
||||
pub(crate) use self::solana::executor_solana_core_vote_build_prepared_plan;
|
||||
/// Crate-root access to `build_prepared_plan` from `zk_elgamal`.
|
||||
pub(crate) use self::solana::executor_solana_core_zk_elgamal_build_prepared_plan;
|
||||
/// Canonical SPL Memo tracing target.
|
||||
pub(crate) use self::spl::EX_SPL_MEMO_TRACING_TARGET;
|
||||
/// Internal SPL Memo plan builder.
|
||||
pub(crate) use self::spl::executor_spl_memo_build_prepared_plan;
|
||||
|
||||
#[cfg(test)]
|
||||
mod reserved_executor_tests {
|
||||
#[test]
|
||||
fn every_reserved_executor_is_registered_and_builds_only_an_empty_plan() {
|
||||
let executors: [&dyn crate::ExApiInstructionExecutor; 103] = [
|
||||
let executors: [&dyn crate::ExApiInstructionExecutor; 102] = [
|
||||
&crate::ExAdapterSaberDecimalWrapperExecutor,
|
||||
&crate::ExAdminJupiterLockExecutor,
|
||||
&crate::ExAdminPumpFeesExecutor,
|
||||
@@ -661,7 +688,6 @@ mod reserved_executor_tests {
|
||||
&crate::ExSplAccountCompressionExecutor,
|
||||
&crate::ExSplAssociatedTokenAccountExecutor,
|
||||
&crate::ExSplElgamalRegistryExecutor,
|
||||
&crate::ExSplMemoExecutor,
|
||||
&crate::ExSplNoopExecutor,
|
||||
&crate::ExSplSinglePoolExecutor,
|
||||
&crate::ExSplStakePoolExecutor,
|
||||
|
||||
15
kb-lib/src/executor/safety.rs
Normal file
15
kb-lib/src/executor/safety.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
// file: kb-lib/src/executor/safety.rs
|
||||
// version: 1
|
||||
|
||||
//! Shared execution safety policy.
|
||||
|
||||
mod evaluation;
|
||||
|
||||
/// Exposes the stateless execution safety checker.
|
||||
pub use self::evaluation::ExSafetyChecker;
|
||||
/// Exposes a safety decision.
|
||||
pub use self::evaluation::ExSafetyDecision;
|
||||
/// Exposes a complete safety evaluation.
|
||||
pub use self::evaluation::ExSafetyEvaluation;
|
||||
/// Exposes one safety policy violation.
|
||||
pub use self::evaluation::ExSafetyViolation;
|
||||
694
kb-lib/src/executor/safety/evaluation.rs
Normal file
694
kb-lib/src/executor/safety/evaluation.rs
Normal file
@@ -0,0 +1,694 @@
|
||||
// file: kb-lib/src/executor/safety/evaluation.rs
|
||||
// version: 1
|
||||
|
||||
//! Safety checks applied before simulation, signing or sending.
|
||||
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
|
||||
/// Safety decision returned before continuing an execution stage.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_lib/executor/safety/ExSafetyDecision.ts"
|
||||
)]
|
||||
pub enum ExSafetyDecision {
|
||||
/// The plan is not allowed to continue.
|
||||
Deny,
|
||||
/// The plan requires explicit operator confirmation.
|
||||
RequireConfirmation,
|
||||
/// The plan may continue to the requested stage.
|
||||
Allow,
|
||||
}
|
||||
|
||||
/// One stable safety policy violation.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, TS)]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_lib/executor/safety/ExSafetyViolation.ts"
|
||||
)]
|
||||
pub struct ExSafetyViolation {
|
||||
/// Stable violation code.
|
||||
pub code: std::string::String,
|
||||
/// Human-readable violation message.
|
||||
pub message: std::string::String,
|
||||
}
|
||||
|
||||
/// Complete safety evaluation for one execution stage.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, TS)]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_lib/executor/safety/ExSafetyEvaluation.ts"
|
||||
)]
|
||||
pub struct ExSafetyEvaluation {
|
||||
/// Aggregate decision.
|
||||
pub decision: crate::ExSafetyDecision,
|
||||
/// Violations that produced a denial or confirmation requirement.
|
||||
pub violations: std::vec::Vec<crate::ExSafetyViolation>,
|
||||
}
|
||||
|
||||
/// Stateless execution safety checker.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ExSafetyChecker;
|
||||
|
||||
impl crate::ExSafetyChecker {
|
||||
/// Evaluates a legacy reserved plan conservatively.
|
||||
pub fn evaluate_plan(
|
||||
&self,
|
||||
_plan: &crate::ExApiExecutionPlan,
|
||||
) -> kb_core::Result<crate::ExSafetyDecision> {
|
||||
return std::result::Result::Ok(crate::ExSafetyDecision::RequireConfirmation);
|
||||
}
|
||||
|
||||
/// Evaluates whether a typed plan may proceed to RPC simulation.
|
||||
pub fn evaluate_prepared_plan(
|
||||
&self,
|
||||
plan: &crate::ExApiPreparedExecutionPlan,
|
||||
) -> kb_core::Result<crate::ExSafetyEvaluation> {
|
||||
let mut violations = std::vec::Vec::new();
|
||||
if plan.instructions.is_empty() {
|
||||
push_violation(
|
||||
&mut violations,
|
||||
"execution_plan_has_no_instructions",
|
||||
"the prepared execution plan contains no instruction",
|
||||
);
|
||||
}
|
||||
if plan.required_signers.is_empty() {
|
||||
push_violation(
|
||||
&mut violations,
|
||||
"execution_plan_has_no_signers",
|
||||
"the prepared execution plan declares no required signer",
|
||||
);
|
||||
}
|
||||
if plan.policy.simulation != crate::ExApiExecutionSimulationPolicy::Required {
|
||||
push_violation(
|
||||
&mut violations,
|
||||
"execution_simulation_not_required",
|
||||
"simulation must be required before signing or sending",
|
||||
);
|
||||
}
|
||||
validate_blockhash_policy(plan, &mut violations);
|
||||
validate_signer_contract(plan, &mut violations);
|
||||
validate_authorized_signers(plan, &mut violations);
|
||||
validate_cost_limits(plan, &mut violations);
|
||||
if !violations.is_empty() {
|
||||
return std::result::Result::Ok(crate::ExSafetyEvaluation {
|
||||
decision: crate::ExSafetyDecision::Deny,
|
||||
violations,
|
||||
});
|
||||
}
|
||||
if plan.policy.cluster.expected_cluster == crate::ExApiExecutionCluster::Mainnet {
|
||||
if !plan.policy.cluster.allow_mainnet {
|
||||
push_violation(
|
||||
&mut violations,
|
||||
"execution_mainnet_disabled",
|
||||
"mainnet execution is disabled by the cluster policy",
|
||||
);
|
||||
return std::result::Result::Ok(crate::ExSafetyEvaluation {
|
||||
decision: crate::ExSafetyDecision::Deny,
|
||||
violations,
|
||||
});
|
||||
}
|
||||
if !plan.policy.cluster.mainnet_confirmation {
|
||||
push_violation(
|
||||
&mut violations,
|
||||
"execution_mainnet_confirmation_required",
|
||||
"mainnet execution requires explicit operator confirmation",
|
||||
);
|
||||
return std::result::Result::Ok(crate::ExSafetyEvaluation {
|
||||
decision: crate::ExSafetyDecision::RequireConfirmation,
|
||||
violations,
|
||||
});
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(crate::ExSafetyEvaluation {
|
||||
decision: crate::ExSafetyDecision::Allow,
|
||||
violations,
|
||||
});
|
||||
}
|
||||
|
||||
/// Evaluates whether a simulated plan may proceed to signing and sending.
|
||||
pub fn evaluate_send(
|
||||
&self,
|
||||
plan: &crate::ExApiPreparedExecutionPlan,
|
||||
simulation: &crate::ExApiExecutionSimulationResult,
|
||||
) -> kb_core::Result<crate::ExSafetyEvaluation> {
|
||||
let plan_evaluation = match self.evaluate_prepared_plan(plan) {
|
||||
std::result::Result::Ok(evaluation) => evaluation,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if plan_evaluation.decision == crate::ExSafetyDecision::Deny {
|
||||
return std::result::Result::Ok(plan_evaluation);
|
||||
}
|
||||
let mut violations = plan_evaluation.violations;
|
||||
if plan.policy.dry_run {
|
||||
push_violation(
|
||||
&mut violations,
|
||||
"execution_dry_run_enabled",
|
||||
"dry-run mode forbids transaction signing and sending",
|
||||
);
|
||||
}
|
||||
if !simulation.simulated {
|
||||
push_violation(
|
||||
&mut violations,
|
||||
"execution_simulation_missing",
|
||||
"an RPC simulation result is required before sending",
|
||||
);
|
||||
} else {
|
||||
if !simulation.success {
|
||||
push_violation(
|
||||
&mut violations,
|
||||
"execution_simulation_failed",
|
||||
"the simulated transaction did not complete successfully",
|
||||
);
|
||||
}
|
||||
if simulation.replacement_blockhash.is_some()
|
||||
|| simulation.replacement_last_valid_block_height.is_some()
|
||||
{
|
||||
push_violation(
|
||||
&mut violations,
|
||||
"execution_simulation_replaced_blockhash",
|
||||
"a simulation with a replacement blockhash cannot authorize signing or sending the original message",
|
||||
);
|
||||
}
|
||||
validate_simulation_context(plan, simulation, &mut violations);
|
||||
}
|
||||
validate_simulated_fee(plan, simulation, &mut violations);
|
||||
if !violations.is_empty() {
|
||||
let decision = if violations.iter().all(|violation| {
|
||||
return violation.code == "execution_mainnet_confirmation_required";
|
||||
}) {
|
||||
crate::ExSafetyDecision::RequireConfirmation
|
||||
} else {
|
||||
crate::ExSafetyDecision::Deny
|
||||
};
|
||||
return std::result::Result::Ok(crate::ExSafetyEvaluation { decision, violations });
|
||||
}
|
||||
return std::result::Result::Ok(crate::ExSafetyEvaluation {
|
||||
decision: crate::ExSafetyDecision::Allow,
|
||||
violations,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_blockhash_policy(
|
||||
plan: &crate::ExApiPreparedExecutionPlan,
|
||||
violations: &mut std::vec::Vec<crate::ExSafetyViolation>,
|
||||
) {
|
||||
match plan.policy.blockhash.kind {
|
||||
crate::ExApiExecutionBlockhashKind::Latest => {
|
||||
if plan.policy.blockhash.max_age_slots == std::option::Option::Some(0)
|
||||
|| plan.policy.blockhash.max_age_slots.is_none()
|
||||
{
|
||||
push_violation(
|
||||
violations,
|
||||
"execution_blockhash_age_missing",
|
||||
"a positive maximum blockhash age is required",
|
||||
);
|
||||
}
|
||||
if plan.policy.blockhash.nonce_account.is_some()
|
||||
|| plan.policy.blockhash.nonce_authority.is_some()
|
||||
{
|
||||
push_violation(
|
||||
violations,
|
||||
"execution_latest_blockhash_has_nonce_fields",
|
||||
"latest blockhash policy must not carry durable nonce fields",
|
||||
);
|
||||
}
|
||||
},
|
||||
crate::ExApiExecutionBlockhashKind::DurableNonce => {
|
||||
if plan.policy.blockhash.nonce_account.is_none()
|
||||
|| plan.policy.blockhash.nonce_authority.is_none()
|
||||
{
|
||||
push_violation(
|
||||
violations,
|
||||
"execution_durable_nonce_context_missing",
|
||||
"durable nonce policy requires both account and authority",
|
||||
);
|
||||
}
|
||||
if plan.policy.blockhash.max_age_slots.is_some() {
|
||||
push_violation(
|
||||
violations,
|
||||
"execution_durable_nonce_has_blockhash_age",
|
||||
"durable nonce policy must not carry a recent blockhash age",
|
||||
);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_signer_contract(
|
||||
plan: &crate::ExApiPreparedExecutionPlan,
|
||||
violations: &mut std::vec::Vec<crate::ExSafetyViolation>,
|
||||
) {
|
||||
let mut unique_signers = std::collections::BTreeSet::new();
|
||||
for required in &plan.required_signers {
|
||||
if !unique_signers.insert(required.pubkey.0.as_str()) {
|
||||
push_violation(
|
||||
violations,
|
||||
"execution_required_signer_duplicate",
|
||||
format!("required signer {} is declared more than once", required.pubkey.0),
|
||||
);
|
||||
}
|
||||
}
|
||||
if !plan
|
||||
.required_signers
|
||||
.iter()
|
||||
.any(|required| return required.pubkey == plan.fee_payer)
|
||||
{
|
||||
push_violation(
|
||||
violations,
|
||||
"execution_fee_payer_not_declared",
|
||||
"the transaction fee payer is missing from required signers",
|
||||
);
|
||||
}
|
||||
for instruction in &plan.instructions {
|
||||
for account in &instruction.accounts {
|
||||
if account.is_signer
|
||||
&& !plan
|
||||
.required_signers
|
||||
.iter()
|
||||
.any(|required| return required.pubkey == account.pubkey)
|
||||
{
|
||||
push_violation(
|
||||
violations,
|
||||
"execution_instruction_signer_not_declared",
|
||||
format!(
|
||||
"instruction signer {} is missing from required signers",
|
||||
account.pubkey.0
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_authorized_signers(
|
||||
plan: &crate::ExApiPreparedExecutionPlan,
|
||||
violations: &mut std::vec::Vec<crate::ExSafetyViolation>,
|
||||
) {
|
||||
for required in &plan.required_signers {
|
||||
if !plan
|
||||
.policy
|
||||
.authorized_signers
|
||||
.iter()
|
||||
.any(|authorized| return authorized == &required.pubkey)
|
||||
{
|
||||
push_violation(
|
||||
violations,
|
||||
"execution_signer_not_authorized",
|
||||
format!(
|
||||
"required signer {} with role {} is not authorized",
|
||||
required.pubkey.0, required.role
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_cost_limits(
|
||||
plan: &crate::ExApiPreparedExecutionPlan,
|
||||
violations: &mut std::vec::Vec<crate::ExSafetyViolation>,
|
||||
) {
|
||||
match plan.policy.cost_limit.max_fee_lamports {
|
||||
std::option::Option::Some(max_fee_lamports) if max_fee_lamports > 0 => {},
|
||||
std::option::Option::Some(_) | std::option::Option::None => {
|
||||
push_violation(
|
||||
violations,
|
||||
"execution_fee_limit_missing",
|
||||
"a positive transaction fee ceiling is required",
|
||||
);
|
||||
},
|
||||
}
|
||||
if plan.requested_spend_lamports > 0 {
|
||||
match plan.policy.cost_limit.max_spend_lamports {
|
||||
std::option::Option::Some(limit) if plan.requested_spend_lamports <= limit => {},
|
||||
std::option::Option::Some(_) => push_violation(
|
||||
violations,
|
||||
"execution_spend_limit_exceeded",
|
||||
"the requested lamport spend exceeds the configured ceiling",
|
||||
),
|
||||
std::option::Option::None => push_violation(
|
||||
violations,
|
||||
"execution_spend_limit_missing",
|
||||
"a lamport spend ceiling is required for this operation",
|
||||
),
|
||||
}
|
||||
}
|
||||
if let std::option::Option::Some(requested_price) =
|
||||
plan.requested_compute_unit_price_micro_lamports
|
||||
{
|
||||
match plan.policy.cost_limit.max_compute_unit_price_micro_lamports {
|
||||
std::option::Option::Some(limit) if requested_price <= limit => {},
|
||||
std::option::Option::Some(_) => push_violation(
|
||||
violations,
|
||||
"execution_compute_unit_price_limit_exceeded",
|
||||
"the requested compute-unit price exceeds the configured ceiling",
|
||||
),
|
||||
std::option::Option::None => push_violation(
|
||||
violations,
|
||||
"execution_compute_unit_price_limit_missing",
|
||||
"a compute-unit price ceiling is required for this operation",
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_simulation_context(
|
||||
plan: &crate::ExApiPreparedExecutionPlan,
|
||||
simulation: &crate::ExApiExecutionSimulationResult,
|
||||
violations: &mut std::vec::Vec<crate::ExSafetyViolation>,
|
||||
) {
|
||||
if simulation.cluster != plan.policy.cluster.expected_cluster {
|
||||
push_violation(
|
||||
violations,
|
||||
"execution_simulation_cluster_mismatch",
|
||||
"the simulation cluster does not match the execution policy",
|
||||
);
|
||||
}
|
||||
if simulation.blockhash_kind != plan.policy.blockhash.kind {
|
||||
push_violation(
|
||||
violations,
|
||||
"execution_simulation_blockhash_kind_mismatch",
|
||||
"the simulated blockhash source does not match the execution policy",
|
||||
);
|
||||
return;
|
||||
}
|
||||
match plan.policy.blockhash.kind {
|
||||
crate::ExApiExecutionBlockhashKind::Latest => {
|
||||
let max_age_slots = match plan.policy.blockhash.max_age_slots {
|
||||
std::option::Option::Some(max_age_slots) => max_age_slots,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
match simulation.blockhash_age_slots {
|
||||
std::option::Option::Some(age_slots) if age_slots <= max_age_slots => {},
|
||||
std::option::Option::Some(_) => push_violation(
|
||||
violations,
|
||||
"execution_simulation_blockhash_too_old",
|
||||
"the simulated recent blockhash exceeds the configured maximum age",
|
||||
),
|
||||
std::option::Option::None => push_violation(
|
||||
violations,
|
||||
"execution_simulation_blockhash_age_missing",
|
||||
"the simulation adapter did not report the recent blockhash age",
|
||||
),
|
||||
}
|
||||
if simulation.nonce_account.is_some() || simulation.nonce_authority.is_some() {
|
||||
push_violation(
|
||||
violations,
|
||||
"execution_simulation_latest_has_nonce_fields",
|
||||
"a latest-blockhash simulation must not report durable nonce fields",
|
||||
);
|
||||
}
|
||||
},
|
||||
crate::ExApiExecutionBlockhashKind::DurableNonce => {
|
||||
if simulation.blockhash_age_slots.is_some() {
|
||||
push_violation(
|
||||
violations,
|
||||
"execution_simulation_nonce_has_blockhash_age",
|
||||
"a durable nonce simulation must not report recent blockhash age",
|
||||
);
|
||||
}
|
||||
if simulation.nonce_account != plan.policy.blockhash.nonce_account {
|
||||
push_violation(
|
||||
violations,
|
||||
"execution_simulation_nonce_account_mismatch",
|
||||
"the simulated durable nonce account does not match the execution policy",
|
||||
);
|
||||
}
|
||||
if simulation.nonce_authority != plan.policy.blockhash.nonce_authority {
|
||||
push_violation(
|
||||
violations,
|
||||
"execution_simulation_nonce_authority_mismatch",
|
||||
"the simulated durable nonce authority does not match the execution policy",
|
||||
);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_simulated_fee(
|
||||
plan: &crate::ExApiPreparedExecutionPlan,
|
||||
simulation: &crate::ExApiExecutionSimulationResult,
|
||||
violations: &mut std::vec::Vec<crate::ExSafetyViolation>,
|
||||
) {
|
||||
let limit = match plan.policy.cost_limit.max_fee_lamports {
|
||||
std::option::Option::Some(limit) => limit,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
match simulation.estimated_fee_lamports {
|
||||
std::option::Option::Some(fee) if fee <= limit => {},
|
||||
std::option::Option::Some(_) => push_violation(
|
||||
violations,
|
||||
"execution_simulated_fee_limit_exceeded",
|
||||
"the simulated transaction fee exceeds the configured ceiling",
|
||||
),
|
||||
std::option::Option::None => push_violation(
|
||||
violations,
|
||||
"execution_simulated_fee_missing",
|
||||
"the simulation did not report an estimated transaction fee",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn push_violation(
|
||||
violations: &mut std::vec::Vec<crate::ExSafetyViolation>,
|
||||
code: impl std::convert::Into<std::string::String>,
|
||||
message: impl std::convert::Into<std::string::String>,
|
||||
) {
|
||||
violations.push(crate::ExSafetyViolation {
|
||||
code: code.into(),
|
||||
message: message.into(),
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
fn sample_plan() -> crate::ExApiPreparedExecutionPlan {
|
||||
let signer = crate::MdPubkey(std::string::String::from("11111111111111111111111111111111"));
|
||||
return crate::ExApiPreparedExecutionPlan {
|
||||
executor_name: std::string::String::from("sample_executor"),
|
||||
executor_version: std::string::String::from("0.4.2"),
|
||||
intent_id: std::string::String::from("intent-1"),
|
||||
operation_code: std::string::String::from("sample.operation"),
|
||||
fee_payer: signer.clone(),
|
||||
instructions: vec![crate::ExApiPlannedInstruction {
|
||||
program_id: crate::MdProgramId(std::string::String::from(
|
||||
"11111111111111111111111111111111",
|
||||
)),
|
||||
operation_code: std::string::String::from("sample.operation"),
|
||||
accounts: vec![crate::ExApiPlannedAccount {
|
||||
pubkey: signer.clone(),
|
||||
is_signer: true,
|
||||
is_writable: true,
|
||||
}],
|
||||
data: vec![1],
|
||||
}],
|
||||
required_signers: vec![crate::ExApiRequiredSigner {
|
||||
pubkey: signer.clone(),
|
||||
role: std::string::String::from("fee_payer"),
|
||||
}],
|
||||
policy: crate::ExApiExecutionPolicy {
|
||||
cost_limit: crate::ExApiExecutionCostLimit {
|
||||
max_spend_lamports: std::option::Option::Some(100),
|
||||
max_fee_lamports: std::option::Option::Some(10_000),
|
||||
max_compute_unit_price_micro_lamports: std::option::Option::Some(5),
|
||||
},
|
||||
authorized_signers: vec![signer],
|
||||
..crate::ExApiExecutionPolicy::default()
|
||||
},
|
||||
requested_spend_lamports: 100,
|
||||
requested_compute_unit_price_micro_lamports: std::option::Option::None,
|
||||
};
|
||||
}
|
||||
|
||||
fn successful_simulation() -> crate::ExApiExecutionSimulationResult {
|
||||
return crate::ExApiExecutionSimulationResult {
|
||||
simulated: true,
|
||||
success: true,
|
||||
cluster: crate::ExApiExecutionCluster::Devnet,
|
||||
blockhash_kind: crate::ExApiExecutionBlockhashKind::Latest,
|
||||
blockhash_age_slots: std::option::Option::Some(3),
|
||||
replacement_blockhash: std::option::Option::None,
|
||||
replacement_last_valid_block_height: std::option::Option::None,
|
||||
nonce_account: std::option::Option::None,
|
||||
nonce_authority: std::option::Option::None,
|
||||
units_consumed: std::option::Option::Some(500),
|
||||
estimated_fee_lamports: std::option::Option::Some(5_000),
|
||||
logs: std::vec::Vec::new(),
|
||||
error: std::option::Option::None,
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conservative_plan_is_allowed_for_simulation() {
|
||||
let plan = sample_plan();
|
||||
let evaluation = crate::ExSafetyChecker
|
||||
.evaluate_prepared_plan(&plan)
|
||||
.unwrap_or_else(|error| panic!("unexpected error: {error}"));
|
||||
assert_eq!(evaluation.decision, crate::ExSafetyDecision::Allow);
|
||||
assert!(evaluation.violations.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dry_run_blocks_send_after_successful_simulation() {
|
||||
let plan = sample_plan();
|
||||
let simulation = successful_simulation();
|
||||
let evaluation = crate::ExSafetyChecker
|
||||
.evaluate_send(&plan, &simulation)
|
||||
.unwrap_or_else(|error| panic!("unexpected error: {error}"));
|
||||
assert_eq!(evaluation.decision, crate::ExSafetyDecision::Deny);
|
||||
assert!(evaluation.violations.iter().any(|violation| {
|
||||
return violation.code == "execution_dry_run_enabled";
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn successful_simulation_allows_devnet_send_when_dry_run_is_disabled() {
|
||||
let mut plan = sample_plan();
|
||||
plan.policy.dry_run = false;
|
||||
let simulation = successful_simulation();
|
||||
let evaluation = crate::ExSafetyChecker
|
||||
.evaluate_send(&plan, &simulation)
|
||||
.unwrap_or_else(|error| panic!("unexpected error: {error}"));
|
||||
assert_eq!(evaluation.decision, crate::ExSafetyDecision::Allow);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn undeclared_fee_payer_and_instruction_signer_are_denied() {
|
||||
let mut plan = sample_plan();
|
||||
plan.required_signers.clear();
|
||||
let evaluation = crate::ExSafetyChecker
|
||||
.evaluate_prepared_plan(&plan)
|
||||
.unwrap_or_else(|error| panic!("unexpected error: {error}"));
|
||||
assert_eq!(evaluation.decision, crate::ExSafetyDecision::Deny);
|
||||
assert!(evaluation.violations.iter().any(|violation| {
|
||||
return violation.code == "execution_fee_payer_not_declared";
|
||||
}));
|
||||
assert!(evaluation.violations.iter().any(|violation| {
|
||||
return violation.code == "execution_instruction_signer_not_declared";
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_required_signer_is_denied() {
|
||||
let mut plan = sample_plan();
|
||||
let duplicate = plan.required_signers[0].clone();
|
||||
plan.required_signers.push(duplicate);
|
||||
let evaluation = crate::ExSafetyChecker
|
||||
.evaluate_prepared_plan(&plan)
|
||||
.unwrap_or_else(|error| panic!("unexpected error: {error}"));
|
||||
assert_eq!(evaluation.decision, crate::ExSafetyDecision::Deny);
|
||||
assert!(evaluation.violations.iter().any(|violation| {
|
||||
return violation.code == "execution_required_signer_duplicate";
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unauthorized_signer_is_denied() {
|
||||
let mut plan = sample_plan();
|
||||
plan.policy.authorized_signers.clear();
|
||||
let evaluation = crate::ExSafetyChecker
|
||||
.evaluate_prepared_plan(&plan)
|
||||
.unwrap_or_else(|error| panic!("unexpected error: {error}"));
|
||||
assert_eq!(evaluation.decision, crate::ExSafetyDecision::Deny);
|
||||
assert!(evaluation.violations.iter().any(|violation| {
|
||||
return violation.code == "execution_signer_not_authorized";
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spend_above_ceiling_is_denied() {
|
||||
let mut plan = sample_plan();
|
||||
plan.policy.cost_limit.max_spend_lamports = std::option::Option::Some(99);
|
||||
let evaluation = crate::ExSafetyChecker
|
||||
.evaluate_prepared_plan(&plan)
|
||||
.unwrap_or_else(|error| panic!("unexpected error: {error}"));
|
||||
assert_eq!(evaluation.decision, crate::ExSafetyDecision::Deny);
|
||||
assert!(evaluation.violations.iter().any(|violation| {
|
||||
return violation.code == "execution_spend_limit_exceeded";
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mainnet_requires_enablement_and_confirmation() {
|
||||
let mut plan = sample_plan();
|
||||
plan.policy.cluster.expected_cluster = crate::ExApiExecutionCluster::Mainnet;
|
||||
let disabled = crate::ExSafetyChecker
|
||||
.evaluate_prepared_plan(&plan)
|
||||
.unwrap_or_else(|error| panic!("unexpected error: {error}"));
|
||||
assert_eq!(disabled.decision, crate::ExSafetyDecision::Deny);
|
||||
plan.policy.cluster.allow_mainnet = true;
|
||||
let unconfirmed = crate::ExSafetyChecker
|
||||
.evaluate_prepared_plan(&plan)
|
||||
.unwrap_or_else(|error| panic!("unexpected error: {error}"));
|
||||
assert_eq!(unconfirmed.decision, crate::ExSafetyDecision::RequireConfirmation);
|
||||
plan.policy.cluster.mainnet_confirmation = true;
|
||||
let confirmed = crate::ExSafetyChecker
|
||||
.evaluate_prepared_plan(&plan)
|
||||
.unwrap_or_else(|error| panic!("unexpected error: {error}"));
|
||||
assert_eq!(confirmed.decision, crate::ExSafetyDecision::Allow);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simulation_cluster_mismatch_is_denied() {
|
||||
let mut plan = sample_plan();
|
||||
plan.policy.dry_run = false;
|
||||
let mut simulation = successful_simulation();
|
||||
simulation.cluster = crate::ExApiExecutionCluster::Testnet;
|
||||
let evaluation = crate::ExSafetyChecker
|
||||
.evaluate_send(&plan, &simulation)
|
||||
.unwrap_or_else(|error| panic!("unexpected error: {error}"));
|
||||
assert_eq!(evaluation.decision, crate::ExSafetyDecision::Deny);
|
||||
assert!(evaluation.violations.iter().any(|violation| {
|
||||
return violation.code == "execution_simulation_cluster_mismatch";
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stale_simulation_blockhash_is_denied() {
|
||||
let mut plan = sample_plan();
|
||||
plan.policy.dry_run = false;
|
||||
let mut simulation = successful_simulation();
|
||||
simulation.blockhash_age_slots = std::option::Option::Some(151);
|
||||
let evaluation = crate::ExSafetyChecker
|
||||
.evaluate_send(&plan, &simulation)
|
||||
.unwrap_or_else(|error| panic!("unexpected error: {error}"));
|
||||
assert_eq!(evaluation.decision, crate::ExSafetyDecision::Deny);
|
||||
assert!(evaluation.violations.iter().any(|violation| {
|
||||
return violation.code == "execution_simulation_blockhash_too_old";
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replacement_blockhash_simulation_cannot_authorize_send() {
|
||||
let mut plan = sample_plan();
|
||||
plan.policy.dry_run = false;
|
||||
let mut simulation = successful_simulation();
|
||||
simulation.replacement_blockhash =
|
||||
std::option::Option::Some(std::string::String::from("replacement-blockhash"));
|
||||
simulation.replacement_last_valid_block_height = std::option::Option::Some(200);
|
||||
let evaluation = crate::ExSafetyChecker
|
||||
.evaluate_send(&plan, &simulation)
|
||||
.unwrap_or_else(|error| panic!("unexpected error: {error}"));
|
||||
assert_eq!(evaluation.decision, crate::ExSafetyDecision::Deny);
|
||||
assert!(evaluation.violations.iter().any(|violation| {
|
||||
return violation.code == "execution_simulation_replaced_blockhash";
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simulated_fee_above_ceiling_is_denied() {
|
||||
let mut plan = sample_plan();
|
||||
plan.policy.dry_run = false;
|
||||
let mut simulation = successful_simulation();
|
||||
simulation.estimated_fee_lamports = std::option::Option::Some(10_001);
|
||||
let evaluation = crate::ExSafetyChecker
|
||||
.evaluate_send(&plan, &simulation)
|
||||
.unwrap_or_else(|error| panic!("unexpected error: {error}"));
|
||||
assert_eq!(evaluation.decision, crate::ExSafetyDecision::Deny);
|
||||
assert!(evaluation.violations.iter().any(|violation| {
|
||||
return violation.code == "execution_simulated_fee_limit_exceeded";
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-lib/src/executor/spl.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
//! `spl` executor family.
|
||||
|
||||
@@ -18,8 +18,22 @@ pub use self::account_compression::ExSplAccountCompressionExecutor;
|
||||
pub use self::associated_token_account::ExSplAssociatedTokenAccountExecutor;
|
||||
/// Exposes the reserved `spl/elgamal_registry` executor.
|
||||
pub use self::elgamal_registry::ExSplElgamalRegistryExecutor;
|
||||
/// Exposes the reserved `spl/memo` executor.
|
||||
/// Exposes the stable SPL Memo add-memo operation code.
|
||||
pub use self::memo::EX_SPL_MEMO_ADD_MEMO_OPERATION;
|
||||
/// Exposes the SPL Memo payload bound.
|
||||
pub use self::memo::EX_SPL_MEMO_MAX_MESSAGE_BYTES;
|
||||
/// Exposes the SPL Memo signer bound.
|
||||
pub use self::memo::EX_SPL_MEMO_MAX_SIGNERS;
|
||||
/// Exposes the typed SPL Memo execution intent.
|
||||
pub use self::memo::ExSplMemoExecutionIntent;
|
||||
/// Exposes the SPL Memo executor.
|
||||
pub use self::memo::ExSplMemoExecutor;
|
||||
/// Exposes the exact SPL Memo generation.
|
||||
pub use self::memo::ExSplMemoGeneration;
|
||||
/// Exposes the typed SPL Memo operation.
|
||||
pub use self::memo::ExSplMemoOperation;
|
||||
/// Exposes one ordered SPL Memo signer.
|
||||
pub use self::memo::ExSplMemoSigner;
|
||||
/// Exposes the reserved `spl/noop` executor.
|
||||
pub use self::noop::ExSplNoopExecutor;
|
||||
/// Exposes the reserved `spl/single_pool` executor.
|
||||
@@ -30,3 +44,8 @@ pub use self::stake_pool::ExSplStakePoolExecutor;
|
||||
pub use self::token::ExSplTokenExecutor;
|
||||
/// Exposes the reserved `spl/token2022` executor.
|
||||
pub use self::token2022::ExSplToken2022Executor;
|
||||
|
||||
/// Canonical SPL Memo tracing target.
|
||||
pub(crate) use self::memo::EX_SPL_MEMO_TRACING_TARGET;
|
||||
/// Internal SPL Memo plan builder.
|
||||
pub(crate) use self::memo::executor_spl_memo_build_prepared_plan;
|
||||
|
||||
@@ -1,53 +1,31 @@
|
||||
// file: kb-lib/src/executor/spl/memo.rs
|
||||
// version: 2
|
||||
// version: 4
|
||||
|
||||
//! Reserved executor for `spl_memo`.
|
||||
//! Safe typed executor for `spl_memo`.
|
||||
|
||||
/// Reserved executor for the `spl_memo` program surface.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ExSplMemoExecutor;
|
||||
mod builder;
|
||||
mod constants;
|
||||
mod executor;
|
||||
mod intent;
|
||||
|
||||
impl crate::ExApiInstructionExecutor for crate::ExSplMemoExecutor {
|
||||
fn executor_name(&self) -> &'static str {
|
||||
return "kb_executor_spl_memo";
|
||||
}
|
||||
/// Exposes the SPL Memo executor.
|
||||
pub use self::executor::ExSplMemoExecutor;
|
||||
/// Exposes the stable add-memo operation code.
|
||||
pub use self::intent::EX_SPL_MEMO_ADD_MEMO_OPERATION;
|
||||
/// Exposes the conservative UTF-8 payload bound.
|
||||
pub use self::intent::EX_SPL_MEMO_MAX_MESSAGE_BYTES;
|
||||
/// Exposes the conservative ordered-signer bound.
|
||||
pub use self::intent::EX_SPL_MEMO_MAX_SIGNERS;
|
||||
/// Exposes the typed SPL Memo execution intent.
|
||||
pub use self::intent::ExSplMemoExecutionIntent;
|
||||
/// Exposes the exact SPL Memo program generation.
|
||||
pub use self::intent::ExSplMemoGeneration;
|
||||
/// Exposes the typed SPL Memo operation.
|
||||
pub use self::intent::ExSplMemoOperation;
|
||||
/// Exposes one ordered SPL Memo signer.
|
||||
pub use self::intent::ExSplMemoSigner;
|
||||
|
||||
fn executor_version(&self) -> &'static str {
|
||||
return env!("CARGO_PKG_VERSION");
|
||||
}
|
||||
|
||||
fn program_ids(&self) -> &'static [&'static str] {
|
||||
return &[
|
||||
kb_program_ids::SPL_MEMO_V1_PROGRAM_ID,
|
||||
kb_program_ids::SPL_MEMO_V3_PROGRAM_ID,
|
||||
kb_program_ids::SPL_MEMO_V4_PROGRAM_ID,
|
||||
];
|
||||
}
|
||||
|
||||
fn supports_request(
|
||||
&self,
|
||||
request: &crate::ExApiExecutionRequest,
|
||||
) -> crate::ExApiExecutionSupport {
|
||||
if crate::ExApiInstructionExecutor::handles_program_id(self, &request.program_id) {
|
||||
return crate::ExApiExecutionSupport::Maybe;
|
||||
}
|
||||
return crate::ExApiExecutionSupport::No;
|
||||
}
|
||||
|
||||
fn build_plan(
|
||||
&self,
|
||||
_request: &crate::ExApiExecutionRequest,
|
||||
) -> kb_core::Result<crate::ExApiExecutionPlan> {
|
||||
let payload_json = match crate::executor_api_serialize_payload_json(
|
||||
&serde_json::json!({"status":"reserved_executor","surface":"spl_memo"}),
|
||||
) {
|
||||
std::result::Result::Ok(serialized) => serialized,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(crate::ExApiExecutionPlan {
|
||||
executor_name: "kb_executor_spl_memo".to_string(),
|
||||
instruction_count: 0,
|
||||
payload_json,
|
||||
});
|
||||
}
|
||||
}
|
||||
/// Internal SPL Memo plan builder.
|
||||
pub(crate) use self::builder::executor_spl_memo_build_prepared_plan;
|
||||
/// Canonical tracing target for SPL Memo execution.
|
||||
pub(crate) use self::constants::EX_SPL_MEMO_TRACING_TARGET;
|
||||
|
||||
499
kb-lib/src/executor/spl/memo/builder.rs
Normal file
499
kb-lib/src/executor/spl/memo/builder.rs
Normal file
@@ -0,0 +1,499 @@
|
||||
// file: kb-lib/src/executor/spl/memo/builder.rs
|
||||
// version: 1
|
||||
|
||||
//! Official SPL Memo instruction builder and conservative plan validation.
|
||||
|
||||
use std::str::FromStr; // rust-rules: trait-import
|
||||
|
||||
pub(crate) fn executor_spl_memo_build_prepared_plan(
|
||||
intent: &crate::ExSplMemoExecutionIntent,
|
||||
) -> kb_core::Result<crate::ExApiPreparedExecutionPlan> {
|
||||
if intent.intent_id.trim().is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_spl_memo_intent_id_empty",
|
||||
"SPL Memo execution intent id must not be empty",
|
||||
));
|
||||
}
|
||||
let fee_payer = match parse_pubkey(&intent.fee_payer, "fee_payer") {
|
||||
std::result::Result::Ok(pubkey) => pubkey,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
match validate_policy(intent) {
|
||||
std::result::Result::Ok(()) => {},
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
let (generation, message, signers) = match &intent.operation {
|
||||
crate::ExSplMemoOperation::AddMemo { generation, message, signers } => {
|
||||
(*generation, message, signers)
|
||||
},
|
||||
};
|
||||
if message.len() > crate::EX_SPL_MEMO_MAX_MESSAGE_BYTES {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_spl_memo_payload_too_large",
|
||||
format!(
|
||||
"SPL Memo payload length {} exceeds the executor limit {}",
|
||||
message.len(),
|
||||
crate::EX_SPL_MEMO_MAX_MESSAGE_BYTES
|
||||
),
|
||||
));
|
||||
}
|
||||
if signers.len() > crate::EX_SPL_MEMO_MAX_SIGNERS {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_spl_memo_signer_limit_exceeded",
|
||||
format!(
|
||||
"SPL Memo signer occurrence count {} exceeds the executor limit {}",
|
||||
signers.len(),
|
||||
crate::EX_SPL_MEMO_MAX_SIGNERS
|
||||
),
|
||||
));
|
||||
}
|
||||
let program_id = match solana_pubkey::Pubkey::from_str(generation.program_id()) {
|
||||
std::result::Result::Ok(pubkey) => pubkey,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_spl_memo_program_id_invalid",
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
let mut signer_pubkeys = std::vec::Vec::with_capacity(signers.len());
|
||||
for signer in signers {
|
||||
let pubkey = match parse_pubkey(&signer.pubkey, "memo_signer") {
|
||||
std::result::Result::Ok(pubkey) => pubkey,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
signer_pubkeys.push(pubkey);
|
||||
}
|
||||
let signer_refs = signer_pubkeys.iter().collect::<std::vec::Vec<_>>();
|
||||
let instruction = spl_memo_interface::instruction::build_memo(
|
||||
&program_id,
|
||||
message.as_bytes(),
|
||||
signer_refs.as_slice(),
|
||||
);
|
||||
let planned_instruction =
|
||||
planned_instruction(crate::EX_SPL_MEMO_ADD_MEMO_OPERATION, &instruction);
|
||||
let required_signers = required_signers(&intent.fee_payer, signers.as_slice());
|
||||
tracing::debug!(
|
||||
target: crate::EX_SPL_MEMO_TRACING_TARGET,
|
||||
action = "build_prepared_plan",
|
||||
intent_id = %intent.intent_id,
|
||||
operation_code = crate::EX_SPL_MEMO_ADD_MEMO_OPERATION,
|
||||
program_id = generation.program_id(),
|
||||
payload_length = message.len(),
|
||||
signer_occurrence_count = signers.len(),
|
||||
required_signer_count = required_signers.len(),
|
||||
dry_run = intent.policy.dry_run,
|
||||
"built SPL Memo execution plan"
|
||||
);
|
||||
return std::result::Result::Ok(crate::ExApiPreparedExecutionPlan {
|
||||
executor_name: std::string::String::from("kb_executor_spl_memo"),
|
||||
executor_version: std::string::String::from(env!("CARGO_PKG_VERSION")),
|
||||
intent_id: intent.intent_id.clone(),
|
||||
operation_code: std::string::String::from(crate::EX_SPL_MEMO_ADD_MEMO_OPERATION),
|
||||
fee_payer: crate::MdPubkey(fee_payer.to_string()),
|
||||
instructions: vec![planned_instruction],
|
||||
required_signers,
|
||||
policy: intent.policy.clone(),
|
||||
requested_spend_lamports: 0,
|
||||
requested_compute_unit_price_micro_lamports: std::option::Option::None,
|
||||
});
|
||||
}
|
||||
|
||||
fn validate_policy(intent: &crate::ExSplMemoExecutionIntent) -> kb_core::Result<()> {
|
||||
if intent.policy.simulation != crate::ExApiExecutionSimulationPolicy::Required {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_spl_memo_simulation_required",
|
||||
"SPL Memo execution requires simulation before signing or sending",
|
||||
));
|
||||
}
|
||||
match intent.policy.cost_limit.max_fee_lamports {
|
||||
std::option::Option::Some(limit) if limit > 0 => {},
|
||||
std::option::Option::Some(_) | std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_spl_memo_fee_limit_missing",
|
||||
"SPL Memo execution requires a positive transaction fee ceiling",
|
||||
));
|
||||
},
|
||||
}
|
||||
let validation = &intent.policy.post_execution_validation;
|
||||
if !validation.canonical_insert_required
|
||||
|| !validation.core_extraction_required
|
||||
|| !validation.decode_replay_required
|
||||
|| !validation.materialization_required
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_spl_memo_post_validation_required",
|
||||
"SPL Memo execution requires canonical insertion, core extraction, decode replay and materialization validation",
|
||||
));
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn parse_pubkey(pubkey: &crate::MdPubkey, field: &str) -> kb_core::Result<solana_pubkey::Pubkey> {
|
||||
if pubkey.0.trim().is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_spl_memo_pubkey_empty",
|
||||
format!("SPL Memo {field} public key must not be empty"),
|
||||
));
|
||||
}
|
||||
return match solana_pubkey::Pubkey::from_str(pubkey.0.as_str()) {
|
||||
std::result::Result::Ok(parsed) => std::result::Result::Ok(parsed),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_spl_memo_pubkey_invalid",
|
||||
format!("invalid SPL Memo {field} public key: {error}"),
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
fn planned_instruction(
|
||||
operation_code: &str,
|
||||
instruction: &solana_instruction::Instruction,
|
||||
) -> crate::ExApiPlannedInstruction {
|
||||
return crate::ExApiPlannedInstruction {
|
||||
program_id: crate::MdProgramId(instruction.program_id.to_string()),
|
||||
operation_code: std::string::String::from(operation_code),
|
||||
accounts: instruction
|
||||
.accounts
|
||||
.iter()
|
||||
.map(|account| {
|
||||
return crate::ExApiPlannedAccount {
|
||||
pubkey: crate::MdPubkey(account.pubkey.to_string()),
|
||||
is_signer: account.is_signer,
|
||||
is_writable: account.is_writable,
|
||||
};
|
||||
})
|
||||
.collect(),
|
||||
data: instruction.data.clone(),
|
||||
};
|
||||
}
|
||||
|
||||
fn required_signers(
|
||||
fee_payer: &crate::MdPubkey,
|
||||
signers: &[crate::ExSplMemoSigner],
|
||||
) -> std::vec::Vec<crate::ExApiRequiredSigner> {
|
||||
let mut required = vec![crate::ExApiRequiredSigner {
|
||||
pubkey: fee_payer.clone(),
|
||||
role: std::string::String::from("fee_payer"),
|
||||
}];
|
||||
for signer in signers {
|
||||
if required.iter().any(|candidate| return candidate.pubkey == signer.pubkey) {
|
||||
continue;
|
||||
}
|
||||
required.push(crate::ExApiRequiredSigner {
|
||||
pubkey: signer.pubkey.clone(),
|
||||
role: std::string::String::from("memo_signer"),
|
||||
});
|
||||
}
|
||||
return required;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use std::str::FromStr; // rust-rules: trait-import
|
||||
|
||||
fn pubkey(value: &str) -> crate::MdPubkey {
|
||||
return crate::MdPubkey(std::string::String::from(value));
|
||||
}
|
||||
|
||||
fn intent(
|
||||
generation: crate::ExSplMemoGeneration,
|
||||
message: &str,
|
||||
signers: &[&str],
|
||||
dry_run: bool,
|
||||
) -> crate::ExSplMemoExecutionIntent {
|
||||
let fee_payer = kb_program_ids::SYSTEM_PROGRAM_ID;
|
||||
let mut authorized_signers = vec![pubkey(fee_payer)];
|
||||
for signer in signers {
|
||||
let candidate = pubkey(signer);
|
||||
if !authorized_signers.contains(&candidate) {
|
||||
authorized_signers.push(candidate);
|
||||
}
|
||||
}
|
||||
return crate::ExSplMemoExecutionIntent {
|
||||
intent_id: std::string::String::from("memo-intent-1"),
|
||||
fee_payer: pubkey(fee_payer),
|
||||
policy: crate::ExApiExecutionPolicy {
|
||||
cost_limit: crate::ExApiExecutionCostLimit {
|
||||
max_spend_lamports: std::option::Option::Some(0),
|
||||
max_fee_lamports: std::option::Option::Some(10_000),
|
||||
max_compute_unit_price_micro_lamports: std::option::Option::None,
|
||||
},
|
||||
authorized_signers,
|
||||
dry_run,
|
||||
post_execution_validation: crate::ExApiPostExecutionValidationPolicy {
|
||||
canonical_insert_required: true,
|
||||
core_extraction_required: true,
|
||||
decode_replay_required: true,
|
||||
materialization_required: true,
|
||||
},
|
||||
..crate::ExApiExecutionPolicy::default()
|
||||
},
|
||||
operation: crate::ExSplMemoOperation::AddMemo {
|
||||
generation,
|
||||
message: std::string::String::from(message),
|
||||
signers: signers
|
||||
.iter()
|
||||
.map(|value| {
|
||||
return crate::ExSplMemoSigner { pubkey: pubkey(value) };
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn assert_matches_official(
|
||||
plan: &crate::ExApiPreparedExecutionPlan,
|
||||
generation: crate::ExSplMemoGeneration,
|
||||
message: &str,
|
||||
signers: &[&str],
|
||||
) {
|
||||
let program_id = solana_pubkey::Pubkey::from_str(generation.program_id())
|
||||
.unwrap_or_else(|error| panic!("invalid program fixture: {error}"));
|
||||
let signer_pubkeys = signers
|
||||
.iter()
|
||||
.map(|value| {
|
||||
return solana_pubkey::Pubkey::from_str(value)
|
||||
.unwrap_or_else(|error| panic!("invalid signer fixture: {error}"));
|
||||
})
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
let signer_refs = signer_pubkeys.iter().collect::<std::vec::Vec<_>>();
|
||||
let official = spl_memo_interface::instruction::build_memo(
|
||||
&program_id,
|
||||
message.as_bytes(),
|
||||
signer_refs.as_slice(),
|
||||
);
|
||||
assert_eq!(plan.instructions.len(), 1);
|
||||
let actual = &plan.instructions[0];
|
||||
assert_eq!(actual.program_id.0, official.program_id.to_string());
|
||||
assert_eq!(actual.data, official.data);
|
||||
assert_eq!(actual.accounts.len(), official.accounts.len());
|
||||
for (actual_account, official_account) in
|
||||
actual.accounts.iter().zip(official.accounts.iter())
|
||||
{
|
||||
assert_eq!(actual_account.pubkey.0, official_account.pubkey.to_string());
|
||||
assert_eq!(actual_account.is_signer, official_account.is_signer);
|
||||
assert_eq!(actual_account.is_writable, official_account.is_writable);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn current_generation_matches_the_official_explicit_program_builder() {
|
||||
let generation = crate::ExSplMemoGeneration::V4;
|
||||
let intent = intent(generation, "memo 🐆", &[kb_program_ids::VOTE_PROGRAM_ID], true);
|
||||
let plan = crate::ExApiTypedInstructionExecutor::build_prepared_plan(
|
||||
&crate::ExSplMemoExecutor,
|
||||
&intent,
|
||||
)
|
||||
.unwrap_or_else(|error| panic!("Memo plan failed: {error}"));
|
||||
assert_matches_official(&plan, generation, "memo 🐆", &[kb_program_ids::VOTE_PROGRAM_ID]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_payload_and_zero_signers_are_constructible() {
|
||||
let intent = intent(crate::ExSplMemoGeneration::V4, "", &[], false);
|
||||
let plan = crate::ExApiTypedInstructionExecutor::build_prepared_plan(
|
||||
&crate::ExSplMemoExecutor,
|
||||
&intent,
|
||||
)
|
||||
.unwrap_or_else(|error| panic!("empty Memo plan failed: {error}"));
|
||||
assert!(plan.instructions[0].data.is_empty());
|
||||
assert!(plan.instructions[0].accounts.is_empty());
|
||||
assert_eq!(plan.required_signers.len(), 1);
|
||||
assert_eq!(plan.requested_spend_lamports, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_signers_remain_ordered_readonly_accounts_but_required_signers_are_unique() {
|
||||
let first = kb_program_ids::VOTE_PROGRAM_ID;
|
||||
let second = kb_program_ids::STAKE_PROGRAM_ID;
|
||||
let intent =
|
||||
intent(crate::ExSplMemoGeneration::V4, "ordered", &[first, second, first], false);
|
||||
let plan = crate::ExApiTypedInstructionExecutor::build_prepared_plan(
|
||||
&crate::ExSplMemoExecutor,
|
||||
&intent,
|
||||
)
|
||||
.unwrap_or_else(|error| panic!("ordered Memo plan failed: {error}"));
|
||||
let accounts = &plan.instructions[0].accounts;
|
||||
assert_eq!(accounts.len(), 3);
|
||||
assert_eq!(accounts[0].pubkey.0, first);
|
||||
assert_eq!(accounts[1].pubkey.0, second);
|
||||
assert_eq!(accounts[2].pubkey.0, first);
|
||||
assert!(accounts.iter().all(|account| return account.is_signer));
|
||||
assert!(accounts.iter().all(|account| return !account.is_writable));
|
||||
assert_eq!(plan.required_signers.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepared_plan_passes_common_pre_simulation_safety() {
|
||||
let intent = intent(
|
||||
crate::ExSplMemoGeneration::V4,
|
||||
"safe plan",
|
||||
&[kb_program_ids::VOTE_PROGRAM_ID],
|
||||
true,
|
||||
);
|
||||
let plan = crate::ExApiTypedInstructionExecutor::build_prepared_plan(
|
||||
&crate::ExSplMemoExecutor,
|
||||
&intent,
|
||||
)
|
||||
.unwrap_or_else(|error| panic!("safe Memo plan failed: {error}"));
|
||||
let evaluation = crate::ExSafetyChecker
|
||||
.evaluate_prepared_plan(&plan)
|
||||
.unwrap_or_else(|error| panic!("safety evaluation failed: {error}"));
|
||||
assert_eq!(evaluation.decision, crate::ExSafetyDecision::Allow);
|
||||
assert!(evaluation.violations.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn current_generation_accepts_non_dry_run_plans() {
|
||||
let generation = crate::ExSplMemoGeneration::V4;
|
||||
let intent = intent(generation, "dry", &[], false);
|
||||
let plan = crate::ExApiTypedInstructionExecutor::build_prepared_plan(
|
||||
&crate::ExSplMemoExecutor,
|
||||
&intent,
|
||||
)
|
||||
.unwrap_or_else(|error| panic!("non-dry-run Memo plan failed: {error}"));
|
||||
assert!(!plan.policy.dry_run);
|
||||
assert_eq!(plan.instructions[0].program_id.0, generation.program_id());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_oversize_payload_and_optional_simulation() {
|
||||
let mut oversize = intent(
|
||||
crate::ExSplMemoGeneration::V4,
|
||||
std::string::String::from_utf8(vec![b'a'; crate::EX_SPL_MEMO_MAX_MESSAGE_BYTES + 1])
|
||||
.unwrap_or_else(|error| panic!("fixture creation failed: {error}"))
|
||||
.as_str(),
|
||||
&[],
|
||||
true,
|
||||
);
|
||||
let error = crate::ExApiTypedInstructionExecutor::build_prepared_plan(
|
||||
&crate::ExSplMemoExecutor,
|
||||
&oversize,
|
||||
)
|
||||
.expect_err("oversize payload must fail");
|
||||
assert_eq!(error.code(), "execution_spl_memo_payload_too_large");
|
||||
|
||||
oversize.operation = crate::ExSplMemoOperation::AddMemo {
|
||||
generation: crate::ExSplMemoGeneration::V4,
|
||||
message: std::string::String::from("valid"),
|
||||
signers: std::vec::Vec::new(),
|
||||
};
|
||||
oversize.policy.simulation = crate::ExApiExecutionSimulationPolicy::Optional;
|
||||
let error = crate::ExApiTypedInstructionExecutor::build_prepared_plan(
|
||||
&crate::ExSplMemoExecutor,
|
||||
&oversize,
|
||||
)
|
||||
.expect_err("optional simulation must fail");
|
||||
assert_eq!(error.code(), "execution_spl_memo_simulation_required");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_cluster_is_constructible_and_mainnet_remains_governed_by_common_safety() {
|
||||
for cluster in [
|
||||
crate::ExApiExecutionCluster::Localnet,
|
||||
crate::ExApiExecutionCluster::Devnet,
|
||||
crate::ExApiExecutionCluster::Testnet,
|
||||
crate::ExApiExecutionCluster::Mainnet,
|
||||
] {
|
||||
let mut intent = intent(crate::ExSplMemoGeneration::V4, "universal", &[], false);
|
||||
intent.policy.cluster.expected_cluster = cluster;
|
||||
let plan = crate::ExApiTypedInstructionExecutor::build_prepared_plan(
|
||||
&crate::ExSplMemoExecutor,
|
||||
&intent,
|
||||
)
|
||||
.unwrap_or_else(|error| panic!("cluster plan failed: {error}"));
|
||||
assert_eq!(plan.policy.cluster.expected_cluster, cluster);
|
||||
if cluster == crate::ExApiExecutionCluster::Mainnet {
|
||||
let evaluation = crate::ExSafetyChecker
|
||||
.evaluate_prepared_plan(&plan)
|
||||
.unwrap_or_else(|error| panic!("Mainnet safety evaluation failed: {error}"));
|
||||
assert_eq!(evaluation.decision, crate::ExSafetyDecision::Deny);
|
||||
assert!(evaluation.violations.iter().any(|violation| {
|
||||
return violation.code == "execution_mainnet_disabled";
|
||||
}));
|
||||
let mut explicitly_enabled = plan.clone();
|
||||
explicitly_enabled.policy.cluster.allow_mainnet = true;
|
||||
explicitly_enabled.policy.cluster.mainnet_confirmation = true;
|
||||
let enabled_evaluation = crate::ExSafetyChecker
|
||||
.evaluate_prepared_plan(&explicitly_enabled)
|
||||
.unwrap_or_else(|error| {
|
||||
panic!("enabled Mainnet safety evaluation failed: {error}")
|
||||
});
|
||||
assert_eq!(enabled_evaluation.decision, crate::ExSafetyDecision::Allow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_missing_fee_cap_incomplete_post_validation_and_signer_overflow() {
|
||||
let mut intent = intent(crate::ExSplMemoGeneration::V4, "policy", &[], true);
|
||||
intent.policy.cost_limit.max_fee_lamports = std::option::Option::None;
|
||||
let error = crate::ExApiTypedInstructionExecutor::build_prepared_plan(
|
||||
&crate::ExSplMemoExecutor,
|
||||
&intent,
|
||||
)
|
||||
.expect_err("missing fee cap must fail");
|
||||
assert_eq!(error.code(), "execution_spl_memo_fee_limit_missing");
|
||||
|
||||
intent.policy.cost_limit.max_fee_lamports = std::option::Option::Some(10_000);
|
||||
intent.policy.post_execution_validation.materialization_required = false;
|
||||
let error = crate::ExApiTypedInstructionExecutor::build_prepared_plan(
|
||||
&crate::ExSplMemoExecutor,
|
||||
&intent,
|
||||
)
|
||||
.expect_err("incomplete post validation must fail");
|
||||
assert_eq!(error.code(), "execution_spl_memo_post_validation_required");
|
||||
|
||||
intent.policy.post_execution_validation.materialization_required = true;
|
||||
intent.operation = crate::ExSplMemoOperation::AddMemo {
|
||||
generation: crate::ExSplMemoGeneration::V4,
|
||||
message: std::string::String::from("bounded"),
|
||||
signers: (0..=crate::EX_SPL_MEMO_MAX_SIGNERS)
|
||||
.map(|_| {
|
||||
return crate::ExSplMemoSigner {
|
||||
pubkey: pubkey(kb_program_ids::VOTE_PROGRAM_ID),
|
||||
};
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
let error = crate::ExApiTypedInstructionExecutor::build_prepared_plan(
|
||||
&crate::ExSplMemoExecutor,
|
||||
&intent,
|
||||
)
|
||||
.expect_err("signer overflow must fail");
|
||||
assert_eq!(error.code(), "execution_spl_memo_signer_limit_exceeded");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn machine_readable_matrix_matches_executor_policy() {
|
||||
let parsed = serde_json::from_str::<serde_json::Value>(include_str!(
|
||||
"../../../../../docs/SPL_MEMO_MATRIX.json"
|
||||
))
|
||||
.unwrap_or_else(|error| panic!("Memo matrix parsing failed: {error}"));
|
||||
let contract = parsed
|
||||
.get("executorContract")
|
||||
.unwrap_or_else(|| panic!("Memo executor contract is missing"));
|
||||
assert_eq!(
|
||||
contract.get("operationCode").and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some(crate::EX_SPL_MEMO_ADD_MEMO_OPERATION)
|
||||
);
|
||||
assert_eq!(
|
||||
contract.get("maximumMessageBytes").and_then(serde_json::Value::as_u64),
|
||||
std::option::Option::Some(crate::EX_SPL_MEMO_MAX_MESSAGE_BYTES as u64)
|
||||
);
|
||||
assert_eq!(
|
||||
contract.get("maximumSignerOccurrences").and_then(serde_json::Value::as_u64),
|
||||
std::option::Option::Some(crate::EX_SPL_MEMO_MAX_SIGNERS as u64)
|
||||
);
|
||||
assert_eq!(
|
||||
contract.get("allClustersConstructible").and_then(serde_json::Value::as_bool),
|
||||
std::option::Option::Some(true)
|
||||
);
|
||||
assert_eq!(
|
||||
contract
|
||||
.get("historicalGenerationSendEnabled")
|
||||
.and_then(serde_json::Value::as_bool),
|
||||
std::option::Option::Some(false)
|
||||
);
|
||||
}
|
||||
}
|
||||
7
kb-lib/src/executor/spl/memo/constants.rs
Normal file
7
kb-lib/src/executor/spl/memo/constants.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
// file: kb-lib/src/executor/spl/memo/constants.rs
|
||||
// version: 1
|
||||
|
||||
//! Local constants for the `kb_executor_spl_memo` crate. Program identifiers live in `kb_program_ids`.
|
||||
|
||||
/// Canonical tracing target for this crate.
|
||||
pub(crate) const EX_SPL_MEMO_TRACING_TARGET: &str = "kb-lib.executor.spl.memo";
|
||||
314
kb-lib/src/executor/spl/memo/executor.rs
Normal file
314
kb-lib/src/executor/spl/memo/executor.rs
Normal file
@@ -0,0 +1,314 @@
|
||||
// file: kb-lib/src/executor/spl/memo/executor.rs
|
||||
// version: 1
|
||||
|
||||
//! Exact capability dispatch and typed plan construction for SPL Memo.
|
||||
|
||||
/// SPL Memo executor implementation.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ExSplMemoExecutor;
|
||||
|
||||
impl crate::ExSplMemoExecutor {
|
||||
fn exact_capability(
|
||||
&self,
|
||||
program_id: &crate::MdProgramId,
|
||||
operation_code: &str,
|
||||
) -> crate::ExApiExecutionCapability {
|
||||
let generation = match crate::ExSplMemoGeneration::from_program_id(program_id.0.as_str()) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return crate::ExApiExecutionCapability::unsupported(
|
||||
"execution_spl_memo_program_not_owned",
|
||||
format!("program {} is not owned by kb_executor_spl_memo", program_id.0),
|
||||
);
|
||||
},
|
||||
};
|
||||
if operation_code != crate::EX_SPL_MEMO_ADD_MEMO_OPERATION {
|
||||
return crate::ExApiExecutionCapability::unsupported(
|
||||
"execution_spl_memo_operation_unsupported",
|
||||
format!("SPL Memo operation {operation_code} is not implemented"),
|
||||
);
|
||||
}
|
||||
if generation != crate::ExSplMemoGeneration::V4 {
|
||||
return crate::ExApiExecutionCapability::unsupported(
|
||||
"execution_spl_memo_historical_generation_decode_only",
|
||||
format!(
|
||||
"SPL Memo generation {generation:?} is historical decode-only; only current or experimental generations are executable"
|
||||
),
|
||||
);
|
||||
}
|
||||
return crate::ExApiExecutionCapability::supported(operation_code);
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::ExApiTypedInstructionExecutor for crate::ExSplMemoExecutor {
|
||||
type Intent = crate::ExSplMemoExecutionIntent;
|
||||
|
||||
fn capability(
|
||||
&self,
|
||||
program_id: &crate::MdProgramId,
|
||||
operation_code: &str,
|
||||
) -> crate::ExApiExecutionCapability {
|
||||
return self.exact_capability(program_id, operation_code);
|
||||
}
|
||||
|
||||
fn build_prepared_plan(
|
||||
&self,
|
||||
intent: &Self::Intent,
|
||||
) -> kb_core::Result<crate::ExApiPreparedExecutionPlan> {
|
||||
let program_id = crate::MdProgramId(std::string::String::from(
|
||||
intent.operation.generation().program_id(),
|
||||
));
|
||||
return match self.exact_capability(&program_id, intent.operation.operation_code()) {
|
||||
crate::ExApiExecutionCapability::Supported { operation_code: _ } => {
|
||||
crate::executor_spl_memo_build_prepared_plan(intent)
|
||||
},
|
||||
crate::ExApiExecutionCapability::Unsupported { reason_code, reason } => {
|
||||
std::result::Result::Err(kb_core::Error::new(reason_code, reason))
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::ExApiInstructionExecutor for crate::ExSplMemoExecutor {
|
||||
fn executor_name(&self) -> &'static str {
|
||||
return "kb_executor_spl_memo";
|
||||
}
|
||||
|
||||
fn executor_version(&self) -> &'static str {
|
||||
return env!("CARGO_PKG_VERSION");
|
||||
}
|
||||
|
||||
fn program_ids(&self) -> &'static [&'static str] {
|
||||
return &[
|
||||
kb_program_ids::SPL_MEMO_V1_PROGRAM_ID,
|
||||
kb_program_ids::SPL_MEMO_V3_PROGRAM_ID,
|
||||
kb_program_ids::SPL_MEMO_V4_PROGRAM_ID,
|
||||
];
|
||||
}
|
||||
|
||||
fn supports_request(
|
||||
&self,
|
||||
request: &crate::ExApiExecutionRequest,
|
||||
) -> crate::ExApiExecutionSupport {
|
||||
return match self.exact_capability(&request.program_id, &request.operation_code) {
|
||||
crate::ExApiExecutionCapability::Supported { operation_code: _ } => {
|
||||
crate::ExApiExecutionSupport::Yes
|
||||
},
|
||||
crate::ExApiExecutionCapability::Unsupported { reason_code: _, reason: _ } => {
|
||||
crate::ExApiExecutionSupport::No
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn build_plan(
|
||||
&self,
|
||||
request: &crate::ExApiExecutionRequest,
|
||||
) -> kb_core::Result<crate::ExApiExecutionPlan> {
|
||||
match self.exact_capability(&request.program_id, &request.operation_code) {
|
||||
crate::ExApiExecutionCapability::Supported { operation_code: _ } => {},
|
||||
crate::ExApiExecutionCapability::Unsupported { reason_code, reason } => {
|
||||
return std::result::Result::Err(kb_core::Error::new(reason_code, reason));
|
||||
},
|
||||
}
|
||||
let intent =
|
||||
match serde_json::from_str::<crate::ExSplMemoExecutionIntent>(&request.payload_json) {
|
||||
std::result::Result::Ok(intent) => intent,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_spl_memo_intent_deserialize_failed",
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
if intent.operation.operation_code() != request.operation_code.as_str() {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_operation_code_mismatch",
|
||||
format!(
|
||||
"request operation {} does not match typed intent operation {}",
|
||||
request.operation_code,
|
||||
intent.operation.operation_code()
|
||||
),
|
||||
));
|
||||
}
|
||||
if intent.operation.generation().program_id() != request.program_id.0.as_str() {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_program_id_mismatch",
|
||||
format!(
|
||||
"request program {} does not match typed intent program {}",
|
||||
request.program_id.0,
|
||||
intent.operation.generation().program_id()
|
||||
),
|
||||
));
|
||||
}
|
||||
let prepared =
|
||||
match crate::ExApiTypedInstructionExecutor::build_prepared_plan(self, &intent) {
|
||||
std::result::Result::Ok(plan) => plan,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let payload_value = match serde_json::to_value(&prepared) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_spl_memo_plan_serialize_failed",
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
let payload_json = match crate::executor_api_serialize_payload_json(&payload_value) {
|
||||
std::result::Result::Ok(serialized) => serialized,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(crate::ExApiExecutionPlan {
|
||||
executor_name: std::string::String::from("kb_executor_spl_memo"),
|
||||
instruction_count: prepared.instructions.len(),
|
||||
payload_json,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
fn request(
|
||||
program_id: &str,
|
||||
operation_code: &str,
|
||||
payload_json: std::string::String,
|
||||
) -> crate::ExApiExecutionRequest {
|
||||
return crate::ExApiExecutionRequest {
|
||||
program_id: crate::MdProgramId(program_id.to_string()),
|
||||
operation_code: operation_code.to_string(),
|
||||
payload_json,
|
||||
};
|
||||
}
|
||||
|
||||
fn pubkey(value: &str) -> crate::MdPubkey {
|
||||
return crate::MdPubkey(std::string::String::from(value));
|
||||
}
|
||||
|
||||
fn intent(
|
||||
generation: crate::ExSplMemoGeneration,
|
||||
message: &str,
|
||||
signers: &[&str],
|
||||
dry_run: bool,
|
||||
) -> crate::ExSplMemoExecutionIntent {
|
||||
let fee_payer = kb_program_ids::SYSTEM_PROGRAM_ID;
|
||||
let mut authorized_signers = vec![pubkey(fee_payer)];
|
||||
for signer in signers {
|
||||
let candidate = pubkey(signer);
|
||||
if !authorized_signers.contains(&candidate) {
|
||||
authorized_signers.push(candidate);
|
||||
}
|
||||
}
|
||||
return crate::ExSplMemoExecutionIntent {
|
||||
intent_id: std::string::String::from("memo-intent-1"),
|
||||
fee_payer: pubkey(fee_payer),
|
||||
policy: crate::ExApiExecutionPolicy {
|
||||
cost_limit: crate::ExApiExecutionCostLimit {
|
||||
max_spend_lamports: std::option::Option::Some(0),
|
||||
max_fee_lamports: std::option::Option::Some(10_000),
|
||||
max_compute_unit_price_micro_lamports: std::option::Option::None,
|
||||
},
|
||||
authorized_signers,
|
||||
dry_run,
|
||||
post_execution_validation: crate::ExApiPostExecutionValidationPolicy {
|
||||
canonical_insert_required: true,
|
||||
core_extraction_required: true,
|
||||
decode_replay_required: true,
|
||||
materialization_required: true,
|
||||
},
|
||||
..crate::ExApiExecutionPolicy::default()
|
||||
},
|
||||
operation: crate::ExSplMemoOperation::AddMemo {
|
||||
generation,
|
||||
message: std::string::String::from(message),
|
||||
signers: signers
|
||||
.iter()
|
||||
.map(|value| {
|
||||
return crate::ExSplMemoSigner { pubkey: pubkey(value) };
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_capabilities_support_only_current_v4_add_memo() {
|
||||
let executor = crate::ExSplMemoExecutor;
|
||||
for program_id in
|
||||
[kb_program_ids::SPL_MEMO_V1_PROGRAM_ID, kb_program_ids::SPL_MEMO_V3_PROGRAM_ID]
|
||||
{
|
||||
let request = request(
|
||||
program_id,
|
||||
crate::EX_SPL_MEMO_ADD_MEMO_OPERATION,
|
||||
std::string::String::from("{}"),
|
||||
);
|
||||
assert_eq!(
|
||||
crate::ExApiInstructionExecutor::supports_request(&executor, &request),
|
||||
crate::ExApiExecutionSupport::No
|
||||
);
|
||||
}
|
||||
let current = request(
|
||||
kb_program_ids::SPL_MEMO_V4_PROGRAM_ID,
|
||||
crate::EX_SPL_MEMO_ADD_MEMO_OPERATION,
|
||||
std::string::String::from("{}"),
|
||||
);
|
||||
assert_eq!(
|
||||
crate::ExApiInstructionExecutor::supports_request(&executor, ¤t),
|
||||
crate::ExApiExecutionSupport::Yes
|
||||
);
|
||||
let unsupported_operation = request(
|
||||
kb_program_ids::SPL_MEMO_V4_PROGRAM_ID,
|
||||
"spl_memo.remove_memo",
|
||||
std::string::String::from("{}"),
|
||||
);
|
||||
assert_eq!(
|
||||
crate::ExApiInstructionExecutor::supports_request(&executor, &unsupported_operation,),
|
||||
crate::ExApiExecutionSupport::No
|
||||
);
|
||||
let foreign = request(
|
||||
kb_program_ids::SYSTEM_PROGRAM_ID,
|
||||
crate::EX_SPL_MEMO_ADD_MEMO_OPERATION,
|
||||
std::string::String::from("{}"),
|
||||
);
|
||||
assert_eq!(
|
||||
crate::ExApiInstructionExecutor::supports_request(&executor, &foreign),
|
||||
crate::ExApiExecutionSupport::No
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generic_request_roundtrips_the_typed_plan() {
|
||||
let intent = intent(
|
||||
crate::ExSplMemoGeneration::V4,
|
||||
"generic",
|
||||
&[kb_program_ids::VOTE_PROGRAM_ID],
|
||||
false,
|
||||
);
|
||||
let payload_json = serde_json::to_string(&intent)
|
||||
.unwrap_or_else(|error| panic!("intent serialization failed: {error}"));
|
||||
let request = request(
|
||||
kb_program_ids::SPL_MEMO_V4_PROGRAM_ID,
|
||||
crate::EX_SPL_MEMO_ADD_MEMO_OPERATION,
|
||||
payload_json,
|
||||
);
|
||||
let plan = crate::ExApiInstructionExecutor::build_plan(&crate::ExSplMemoExecutor, &request)
|
||||
.unwrap_or_else(|error| panic!("generic plan failed: {error}"));
|
||||
assert_eq!(plan.instruction_count, 1);
|
||||
let prepared =
|
||||
serde_json::from_str::<crate::ExApiPreparedExecutionPlan>(&plan.payload_json)
|
||||
.unwrap_or_else(|error| panic!("prepared plan parsing failed: {error}"));
|
||||
assert_eq!(prepared.instructions[0].data, b"generic");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn historical_generations_are_decode_only_with_stable_reason() {
|
||||
for generation in [crate::ExSplMemoGeneration::V1, crate::ExSplMemoGeneration::V3] {
|
||||
let intent = intent(generation, "historical", &[], true);
|
||||
let error = crate::ExApiTypedInstructionExecutor::build_prepared_plan(
|
||||
&crate::ExSplMemoExecutor,
|
||||
&intent,
|
||||
)
|
||||
.expect_err("historical Memo generation must not build a plan");
|
||||
assert_eq!(error.code(), "execution_spl_memo_historical_generation_decode_only");
|
||||
}
|
||||
}
|
||||
}
|
||||
112
kb-lib/src/executor/spl/memo/intent.rs
Normal file
112
kb-lib/src/executor/spl/memo/intent.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
// file: kb-lib/src/executor/spl/memo/intent.rs
|
||||
// version: 1
|
||||
|
||||
//! Typed SPL Memo execution intents.
|
||||
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
|
||||
/// Stable operation code for adding one SPL Memo annotation.
|
||||
pub const EX_SPL_MEMO_ADD_MEMO_OPERATION: &str = "spl_memo.add_memo";
|
||||
/// Conservative UTF-8 payload bound used before transaction assembly.
|
||||
pub const EX_SPL_MEMO_MAX_MESSAGE_BYTES: usize = 566;
|
||||
/// Conservative bound on ordered signer occurrences accepted by one intent.
|
||||
pub const EX_SPL_MEMO_MAX_SIGNERS: usize = 32;
|
||||
|
||||
/// Exact SPL Memo program generation.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_lib/executor/spl/memo/intent/ExSplMemoGeneration.ts"
|
||||
)]
|
||||
pub enum ExSplMemoGeneration {
|
||||
/// Historical v1 program, whose runtime ignores supplied accounts.
|
||||
V1,
|
||||
/// Historical v3 program, whose runtime requires every supplied account to sign.
|
||||
V3,
|
||||
/// Current v4 program, whose runtime requires every supplied account to sign.
|
||||
V4,
|
||||
}
|
||||
|
||||
impl crate::ExSplMemoGeneration {
|
||||
/// Returns the exact Program ID for this generation.
|
||||
pub fn program_id(&self) -> &'static str {
|
||||
return match self {
|
||||
Self::V1 => kb_program_ids::SPL_MEMO_V1_PROGRAM_ID,
|
||||
Self::V3 => kb_program_ids::SPL_MEMO_V3_PROGRAM_ID,
|
||||
Self::V4 => kb_program_ids::SPL_MEMO_V4_PROGRAM_ID,
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn from_program_id(program_id: &str) -> std::option::Option<Self> {
|
||||
return match program_id {
|
||||
kb_program_ids::SPL_MEMO_V1_PROGRAM_ID => std::option::Option::Some(Self::V1),
|
||||
kb_program_ids::SPL_MEMO_V3_PROGRAM_ID => std::option::Option::Some(Self::V3),
|
||||
kb_program_ids::SPL_MEMO_V4_PROGRAM_ID => std::option::Option::Some(Self::V4),
|
||||
_ => std::option::Option::None,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// One ordered signer account supplied to the Memo instruction.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, TS)]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_lib/executor/spl/memo/intent/ExSplMemoSigner.ts"
|
||||
)]
|
||||
pub struct ExSplMemoSigner {
|
||||
/// Signer public key; duplicates are preserved in instruction order.
|
||||
pub pubkey: crate::MdPubkey,
|
||||
}
|
||||
|
||||
/// Typed SPL Memo operation arguments.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, TS)]
|
||||
#[serde(tag = "operation", rename_all = "snake_case")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_lib/executor/spl/memo/intent/ExSplMemoOperation.ts"
|
||||
)]
|
||||
pub enum ExSplMemoOperation {
|
||||
/// Add one exact UTF-8 Memo payload with ordered readonly signer accounts.
|
||||
AddMemo {
|
||||
/// Exact Memo program generation.
|
||||
generation: crate::ExSplMemoGeneration,
|
||||
/// UTF-8 text whose bytes become the complete instruction payload.
|
||||
message: std::string::String,
|
||||
/// Ordered signer accounts. Duplicates are preserved.
|
||||
signers: std::vec::Vec<crate::ExSplMemoSigner>,
|
||||
},
|
||||
}
|
||||
|
||||
impl crate::ExSplMemoOperation {
|
||||
/// Returns the stable operation code.
|
||||
pub fn operation_code(&self) -> &'static str {
|
||||
return match self {
|
||||
Self::AddMemo { .. } => crate::EX_SPL_MEMO_ADD_MEMO_OPERATION,
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the exact requested program generation.
|
||||
pub fn generation(&self) -> crate::ExSplMemoGeneration {
|
||||
return match self {
|
||||
Self::AddMemo { generation, .. } => *generation,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Complete typed intent accepted by the SPL Memo executor.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, TS)]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_lib/executor/spl/memo/intent/ExSplMemoExecutionIntent.ts"
|
||||
)]
|
||||
pub struct ExSplMemoExecutionIntent {
|
||||
/// Stable caller-provided identifier used for logs and replay correlation.
|
||||
pub intent_id: std::string::String,
|
||||
/// Transaction fee payer.
|
||||
pub fee_payer: crate::MdPubkey,
|
||||
/// Conservative execution policy.
|
||||
pub policy: crate::ExApiExecutionPolicy,
|
||||
/// Typed Memo operation and arguments.
|
||||
pub operation: crate::ExSplMemoOperation,
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-lib/src/lib.rs
|
||||
// version: 18
|
||||
// version: 19
|
||||
|
||||
//! Consolidated decoder, executor, materializer and shared model library.
|
||||
#![warn(missing_docs)]
|
||||
@@ -563,6 +563,12 @@ pub use self::executor::EX_SOLANA_CORE_ZK_ELGAMAL_CLOSE_CONTEXT_STATE_OPERATION;
|
||||
pub use self::executor::EX_SOLANA_CORE_ZK_ELGAMAL_VERIFY_FROM_ACCOUNT_OPERATION;
|
||||
/// Exposes the ZK ElGamal inline proof verification operation code.
|
||||
pub use self::executor::EX_SOLANA_CORE_ZK_ELGAMAL_VERIFY_INLINE_OPERATION;
|
||||
/// Exposes the stable SPL Memo add-memo operation code.
|
||||
pub use self::executor::EX_SPL_MEMO_ADD_MEMO_OPERATION;
|
||||
/// Exposes the SPL Memo payload bound.
|
||||
pub use self::executor::EX_SPL_MEMO_MAX_MESSAGE_BYTES;
|
||||
/// Exposes the SPL Memo signer bound.
|
||||
pub use self::executor::EX_SPL_MEMO_MAX_SIGNERS;
|
||||
/// Exposes the reserved `ExAdapterSaberDecimalWrapperExecutor` implementation.
|
||||
pub use self::executor::ExAdapterSaberDecimalWrapperExecutor;
|
||||
/// Exposes the reserved `ExAdminJupiterLockExecutor` implementation.
|
||||
@@ -763,6 +769,14 @@ pub use self::executor::ExRouterOkxLabsV1Executor;
|
||||
pub use self::executor::ExRouterOkxLabsV2Executor;
|
||||
/// Exposes the reserved `ExRwaOndoGlobalMarketsExecutor` implementation.
|
||||
pub use self::executor::ExRwaOndoGlobalMarketsExecutor;
|
||||
/// Exposes the stateless execution safety checker.
|
||||
pub use self::executor::ExSafetyChecker;
|
||||
/// Exposes an execution safety decision.
|
||||
pub use self::executor::ExSafetyDecision;
|
||||
/// Exposes a complete execution safety evaluation.
|
||||
pub use self::executor::ExSafetyEvaluation;
|
||||
/// Exposes one execution safety violation.
|
||||
pub use self::executor::ExSafetyViolation;
|
||||
/// Exposes one Config Program key entry.
|
||||
pub use self::executor::ExSolanaCoreConfigKey;
|
||||
/// Exposes one Ed25519 verification offset record.
|
||||
@@ -797,8 +811,16 @@ pub use self::executor::ExSplAccountCompressionExecutor;
|
||||
pub use self::executor::ExSplAssociatedTokenAccountExecutor;
|
||||
/// Exposes the reserved `ExSplElgamalRegistryExecutor` implementation.
|
||||
pub use self::executor::ExSplElgamalRegistryExecutor;
|
||||
/// Exposes the reserved `ExSplMemoExecutor` implementation.
|
||||
/// Exposes the typed SPL Memo execution intent.
|
||||
pub use self::executor::ExSplMemoExecutionIntent;
|
||||
/// Exposes the SPL Memo executor.
|
||||
pub use self::executor::ExSplMemoExecutor;
|
||||
/// Exposes the exact SPL Memo generation.
|
||||
pub use self::executor::ExSplMemoGeneration;
|
||||
/// Exposes the typed SPL Memo operation.
|
||||
pub use self::executor::ExSplMemoOperation;
|
||||
/// Exposes one ordered SPL Memo signer.
|
||||
pub use self::executor::ExSplMemoSigner;
|
||||
/// Exposes the reserved `ExSplNoopExecutor` implementation.
|
||||
pub use self::executor::ExSplNoopExecutor;
|
||||
/// Exposes the reserved `ExSplSinglePoolExecutor` implementation.
|
||||
@@ -1412,6 +1434,8 @@ pub(crate) use self::decoder::decoder_spl_token2022_entry_for_tag;
|
||||
pub(crate) use self::decoder::decoder_spl_token2022_payload_tag;
|
||||
/// Canonical tracing target for this crate.
|
||||
pub(crate) use self::executor::EX_SOLANA_CORE_TRACING_TARGET;
|
||||
/// Canonical SPL Memo tracing target.
|
||||
pub(crate) use self::executor::EX_SPL_MEMO_TRACING_TARGET;
|
||||
/// Crate-root access to `build_prepared_plan` from `address_lookup_table`.
|
||||
pub(crate) use self::executor::executor_solana_core_address_lookup_table_build_prepared_plan;
|
||||
/// Crate-root access to `build_prepared_plan` from `builder`.
|
||||
@@ -1448,6 +1472,8 @@ pub(crate) use self::executor::executor_solana_core_validate_seeded_address;
|
||||
pub(crate) use self::executor::executor_solana_core_vote_build_prepared_plan;
|
||||
/// Crate-root access to `build_prepared_plan` from `zk_elgamal`.
|
||||
pub(crate) use self::executor::executor_solana_core_zk_elgamal_build_prepared_plan;
|
||||
/// Internal SPL Memo plan builder.
|
||||
pub(crate) use self::executor::executor_spl_memo_build_prepared_plan;
|
||||
/// Canonical processor name for the native and SPL administration materializer.
|
||||
pub(crate) use self::materializer::MT_ADMIN_PROCESSOR_NAME;
|
||||
/// Canonical tracing target for the native and SPL administration materializer.
|
||||
|
||||
Reference in New Issue
Block a user