502 lines
21 KiB
Rust
502 lines
21 KiB
Rust
// file: kb_executor_spl_memo/src/builder.rs
|
|
// version: 6
|
|
|
|
//! Official SPL Memo instruction builder and conservative plan validation.
|
|
|
|
use std::str::FromStr; // rust-rules: trait-import
|
|
|
|
pub(crate) fn build_prepared_plan(
|
|
intent: &crate::SplMemoExecutionIntent,
|
|
) -> kb_core::Result<kb_execution_api::PreparedExecutionPlan> {
|
|
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::SplMemoOperation::AddMemo { generation, message, signers } => {
|
|
(*generation, message, signers)
|
|
},
|
|
};
|
|
if message.len() > crate::MAX_MEMO_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::MAX_MEMO_MESSAGE_BYTES
|
|
),
|
|
));
|
|
}
|
|
if signers.len() > crate::MAX_MEMO_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::MAX_MEMO_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::SPL_MEMO_ADD_MEMO_OPERATION, &instruction);
|
|
let required_signers = required_signers(&intent.fee_payer, signers.as_slice());
|
|
tracing::debug!(
|
|
target: crate::TRACING_TARGET,
|
|
action = "build_prepared_plan",
|
|
intent_id = %intent.intent_id,
|
|
operation_code = crate::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(kb_execution_api::PreparedExecutionPlan {
|
|
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::SPL_MEMO_ADD_MEMO_OPERATION),
|
|
fee_payer: kb_model::Pubkey(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::SplMemoExecutionIntent) -> kb_core::Result<()> {
|
|
if intent.policy.simulation != kb_execution_api::ExecutionSimulationPolicy::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: &kb_model::Pubkey, 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,
|
|
) -> kb_execution_api::PlannedInstruction {
|
|
return kb_execution_api::PlannedInstruction {
|
|
program_id: kb_model::ProgramId(instruction.program_id.to_string()),
|
|
operation_code: std::string::String::from(operation_code),
|
|
accounts: instruction
|
|
.accounts
|
|
.iter()
|
|
.map(|account| {
|
|
return kb_execution_api::PlannedAccount {
|
|
pubkey: kb_model::Pubkey(account.pubkey.to_string()),
|
|
is_signer: account.is_signer,
|
|
is_writable: account.is_writable,
|
|
};
|
|
})
|
|
.collect(),
|
|
data: instruction.data.clone(),
|
|
};
|
|
}
|
|
|
|
fn required_signers(
|
|
fee_payer: &kb_model::Pubkey,
|
|
signers: &[crate::SplMemoSigner],
|
|
) -> std::vec::Vec<kb_execution_api::RequiredSigner> {
|
|
let mut required = vec![kb_execution_api::RequiredSigner {
|
|
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(kb_execution_api::RequiredSigner {
|
|
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) -> kb_model::Pubkey {
|
|
return kb_model::Pubkey(std::string::String::from(value));
|
|
}
|
|
|
|
fn intent(
|
|
generation: crate::SplMemoGeneration,
|
|
message: &str,
|
|
signers: &[&str],
|
|
dry_run: bool,
|
|
) -> crate::SplMemoExecutionIntent {
|
|
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::SplMemoExecutionIntent {
|
|
intent_id: std::string::String::from("memo-intent-1"),
|
|
fee_payer: pubkey(fee_payer),
|
|
policy: kb_execution_api::ExecutionPolicy {
|
|
cost_limit: kb_execution_api::ExecutionCostLimit {
|
|
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: kb_execution_api::PostExecutionValidationPolicy {
|
|
canonical_insert_required: true,
|
|
core_extraction_required: true,
|
|
decode_replay_required: true,
|
|
materialization_required: true,
|
|
},
|
|
..kb_execution_api::ExecutionPolicy::default()
|
|
},
|
|
operation: crate::SplMemoOperation::AddMemo {
|
|
generation,
|
|
message: std::string::String::from(message),
|
|
signers: signers
|
|
.iter()
|
|
.map(|value| {
|
|
return crate::SplMemoSigner { pubkey: pubkey(value) };
|
|
})
|
|
.collect(),
|
|
},
|
|
};
|
|
}
|
|
|
|
fn assert_matches_official(
|
|
plan: &kb_execution_api::PreparedExecutionPlan,
|
|
generation: crate::SplMemoGeneration,
|
|
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::SplMemoGeneration::V4;
|
|
let intent = intent(generation, "memo 🐆", &[kb_program_ids::VOTE_PROGRAM_ID], true);
|
|
let plan = kb_execution_api::TypedInstructionExecutor::build_prepared_plan(
|
|
&crate::SplMemoExecutor,
|
|
&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::SplMemoGeneration::V4, "", &[], false);
|
|
let plan = kb_execution_api::TypedInstructionExecutor::build_prepared_plan(
|
|
&crate::SplMemoExecutor,
|
|
&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::SplMemoGeneration::V4, "ordered", &[first, second, first], false);
|
|
let plan = kb_execution_api::TypedInstructionExecutor::build_prepared_plan(
|
|
&crate::SplMemoExecutor,
|
|
&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::SplMemoGeneration::V4,
|
|
"safe plan",
|
|
&[kb_program_ids::VOTE_PROGRAM_ID],
|
|
true,
|
|
);
|
|
let plan = kb_execution_api::TypedInstructionExecutor::build_prepared_plan(
|
|
&crate::SplMemoExecutor,
|
|
&intent,
|
|
)
|
|
.unwrap_or_else(|error| panic!("safe Memo plan failed: {error}"));
|
|
let evaluation = kb_execution_safety::ExecutionSafetyChecker
|
|
.evaluate_prepared_plan(&plan)
|
|
.unwrap_or_else(|error| panic!("safety evaluation failed: {error}"));
|
|
assert_eq!(evaluation.decision, kb_execution_safety::ExecutionSafetyDecision::Allow);
|
|
assert!(evaluation.violations.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn current_generation_accepts_non_dry_run_plans() {
|
|
let generation = crate::SplMemoGeneration::V4;
|
|
let intent = intent(generation, "dry", &[], false);
|
|
let plan = kb_execution_api::TypedInstructionExecutor::build_prepared_plan(
|
|
&crate::SplMemoExecutor,
|
|
&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::SplMemoGeneration::V4,
|
|
std::string::String::from_utf8(vec![b'a'; crate::MAX_MEMO_MESSAGE_BYTES + 1])
|
|
.unwrap_or_else(|error| panic!("fixture creation failed: {error}"))
|
|
.as_str(),
|
|
&[],
|
|
true,
|
|
);
|
|
let error = kb_execution_api::TypedInstructionExecutor::build_prepared_plan(
|
|
&crate::SplMemoExecutor,
|
|
&oversize,
|
|
)
|
|
.expect_err("oversize payload must fail");
|
|
assert_eq!(error.code(), "execution_spl_memo_payload_too_large");
|
|
|
|
oversize.operation = crate::SplMemoOperation::AddMemo {
|
|
generation: crate::SplMemoGeneration::V4,
|
|
message: std::string::String::from("valid"),
|
|
signers: std::vec::Vec::new(),
|
|
};
|
|
oversize.policy.simulation = kb_execution_api::ExecutionSimulationPolicy::Optional;
|
|
let error = kb_execution_api::TypedInstructionExecutor::build_prepared_plan(
|
|
&crate::SplMemoExecutor,
|
|
&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 [
|
|
kb_execution_api::ExecutionCluster::Localnet,
|
|
kb_execution_api::ExecutionCluster::Devnet,
|
|
kb_execution_api::ExecutionCluster::Testnet,
|
|
kb_execution_api::ExecutionCluster::Mainnet,
|
|
] {
|
|
let mut intent = intent(crate::SplMemoGeneration::V4, "universal", &[], false);
|
|
intent.policy.cluster.expected_cluster = cluster;
|
|
let plan = kb_execution_api::TypedInstructionExecutor::build_prepared_plan(
|
|
&crate::SplMemoExecutor,
|
|
&intent,
|
|
)
|
|
.unwrap_or_else(|error| panic!("cluster plan failed: {error}"));
|
|
assert_eq!(plan.policy.cluster.expected_cluster, cluster);
|
|
if cluster == kb_execution_api::ExecutionCluster::Mainnet {
|
|
let evaluation = kb_execution_safety::ExecutionSafetyChecker
|
|
.evaluate_prepared_plan(&plan)
|
|
.unwrap_or_else(|error| panic!("Mainnet safety evaluation failed: {error}"));
|
|
assert_eq!(evaluation.decision, kb_execution_safety::ExecutionSafetyDecision::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 = kb_execution_safety::ExecutionSafetyChecker
|
|
.evaluate_prepared_plan(&explicitly_enabled)
|
|
.unwrap_or_else(|error| {
|
|
panic!("enabled Mainnet safety evaluation failed: {error}")
|
|
});
|
|
assert_eq!(
|
|
enabled_evaluation.decision,
|
|
kb_execution_safety::ExecutionSafetyDecision::Allow
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_missing_fee_cap_incomplete_post_validation_and_signer_overflow() {
|
|
let mut intent = intent(crate::SplMemoGeneration::V4, "policy", &[], true);
|
|
intent.policy.cost_limit.max_fee_lamports = std::option::Option::None;
|
|
let error = kb_execution_api::TypedInstructionExecutor::build_prepared_plan(
|
|
&crate::SplMemoExecutor,
|
|
&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 = kb_execution_api::TypedInstructionExecutor::build_prepared_plan(
|
|
&crate::SplMemoExecutor,
|
|
&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::SplMemoOperation::AddMemo {
|
|
generation: crate::SplMemoGeneration::V4,
|
|
message: std::string::String::from("bounded"),
|
|
signers: (0..=crate::MAX_MEMO_SIGNERS)
|
|
.map(|_| {
|
|
return crate::SplMemoSigner {
|
|
pubkey: pubkey(kb_program_ids::VOTE_PROGRAM_ID),
|
|
};
|
|
})
|
|
.collect(),
|
|
};
|
|
let error = kb_execution_api::TypedInstructionExecutor::build_prepared_plan(
|
|
&crate::SplMemoExecutor,
|
|
&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::SPL_MEMO_ADD_MEMO_OPERATION)
|
|
);
|
|
assert_eq!(
|
|
contract.get("maximumMessageBytes").and_then(serde_json::Value::as_u64),
|
|
std::option::Option::Some(crate::MAX_MEMO_MESSAGE_BYTES as u64)
|
|
);
|
|
assert_eq!(
|
|
contract.get("maximumSignerOccurrences").and_then(serde_json::Value::as_u64),
|
|
std::option::Option::Some(crate::MAX_MEMO_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)
|
|
);
|
|
}
|
|
}
|