332 lines
14 KiB
Rust
332 lines
14 KiB
Rust
// file: ks-pipeline/src/spl_token_2022_execution_orchestration.rs
|
|
// version: 5
|
|
|
|
//! Final Token-2022 execution-readiness and stateful postcondition contracts.
|
|
|
|
/// Maximum number of distinct transaction signers accepted by one Token-2022 execution envelope.
|
|
pub const MAX_TOKEN_2022_EXECUTION_SIGNERS: usize = 16;
|
|
|
|
/// Result of one stateful postcondition checked after a confirmed Token-2022 transaction.
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum Token2022ExecutionPostconditionStatus {
|
|
/// The observed final state confirms the expected operation effect.
|
|
Confirmed,
|
|
/// The observed final state contradicts the expected operation effect.
|
|
Contradicted,
|
|
/// The operation has no supported stateful postcondition in the current contract.
|
|
NotApplicable,
|
|
}
|
|
|
|
/// One explicit stateful postcondition retained by the execution orchestrator.
|
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
|
pub struct Token2022ExecutionPostcondition {
|
|
/// Stable semantic role such as `mint`, `source`, `destination`, or `registry`.
|
|
pub role: std::string::String,
|
|
/// Canonical account whose final state was inspected.
|
|
pub account: ks_lib::MdPubkey,
|
|
/// Explicit result of the postcondition.
|
|
pub status: crate::Token2022ExecutionPostconditionStatus,
|
|
/// Bounded diagnostic explaining the result without retaining complete account data.
|
|
pub diagnostic: std::string::String,
|
|
}
|
|
|
|
/// Complete deterministic request checked before Token-2022 transaction signing.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct Token2022ExecutionReadinessRequest {
|
|
/// Exact prepared executor plan.
|
|
pub plan: ks_lib::ExApiPreparedExecutionPlan,
|
|
/// Exact hash of the compiled Solana message.
|
|
pub message_hash: std::string::String,
|
|
/// Hash retained by the simulation evidence.
|
|
pub simulated_message_hash: std::string::String,
|
|
/// Whether the exact compiled message was simulated.
|
|
pub simulated: bool,
|
|
/// Whether the exact simulation succeeded.
|
|
pub simulation_succeeded: bool,
|
|
/// Stateful Token-2022 account preflight report.
|
|
pub stateful_preflight: crate::Token2022PreflightReport,
|
|
/// Cryptographic proof context-state preflight report.
|
|
pub cryptographic_preflight: crate::Token2022CryptographicPreflightReport,
|
|
/// Ordered proof orchestration report.
|
|
pub proof_orchestration: crate::Token2022ProofOrchestrationReport,
|
|
/// Public keys actually available to sign the transaction.
|
|
pub resolved_signers: std::vec::Vec<ks_lib::MdPubkey>,
|
|
/// Whether submission was explicitly requested.
|
|
pub submit: bool,
|
|
/// Whether submission received explicit operator confirmation.
|
|
pub operator_confirmed: bool,
|
|
}
|
|
|
|
/// Deterministic execution-readiness report produced before signing.
|
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
|
pub struct Token2022ExecutionReadinessReport {
|
|
/// Stable operation code.
|
|
pub operation_code: std::string::String,
|
|
/// Exact compiled message hash bound to simulation.
|
|
pub message_hash: std::string::String,
|
|
/// Highest RPC context slot across stateful and cryptographic preflights.
|
|
pub context_slot: u64,
|
|
/// Ordered signer public keys required by the plan.
|
|
pub required_signers: std::vec::Vec<ks_lib::MdPubkey>,
|
|
/// Whether transaction signing and submission are authorized.
|
|
pub send_authorized: bool,
|
|
/// Ordered successful checks.
|
|
pub checks: std::vec::Vec<std::string::String>,
|
|
}
|
|
|
|
/// Validates the complete Token-2022 execution envelope before transaction signing.
|
|
pub fn validate_token_2022_execution_readiness(
|
|
request: &crate::Token2022ExecutionReadinessRequest,
|
|
) -> ks_core::Result<crate::Token2022ExecutionReadinessReport> {
|
|
if request.plan.operation_code.trim().is_empty() {
|
|
return std::result::Result::Err(ks_core::Error::config(
|
|
"Token-2022 execution plan operation_code must not be empty",
|
|
));
|
|
}
|
|
if request.plan.operation_code != request.proof_orchestration.operation_code {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"token_2022_execution_operation_mismatch",
|
|
"Token-2022 plan and proof orchestration operation codes must match",
|
|
));
|
|
}
|
|
if request.message_hash.trim().is_empty()
|
|
|| request.simulated_message_hash.trim().is_empty()
|
|
|| request.message_hash != request.simulated_message_hash
|
|
{
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"token_2022_execution_simulation_message_mismatch",
|
|
"Token-2022 simulation must be bound to the exact compiled message hash",
|
|
));
|
|
}
|
|
if !request.simulated || !request.simulation_succeeded {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"token_2022_execution_simulation_required",
|
|
"Token-2022 execution requires one successful exact-message simulation",
|
|
));
|
|
}
|
|
if !request.proof_orchestration.simulation_required {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"token_2022_execution_proof_policy_mismatch",
|
|
"Token-2022 proof orchestration must require simulation",
|
|
));
|
|
}
|
|
if request.stateful_preflight.commitment != "confirmed"
|
|
|| request.cryptographic_preflight.commitment != "confirmed"
|
|
{
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"token_2022_execution_preflight_commitment_mismatch",
|
|
"Token-2022 stateful and cryptographic preflights must use confirmed commitment",
|
|
));
|
|
}
|
|
let required_signers = match validate_signers(
|
|
request.plan.required_signers.as_slice(),
|
|
request.resolved_signers.as_slice(),
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
if request.submit && !request.operator_confirmed {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"token_2022_execution_confirmation_required",
|
|
"Token-2022 submission requires explicit operator confirmation",
|
|
));
|
|
}
|
|
if request.submit && !request.proof_orchestration.send_authorized {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"token_2022_execution_proof_send_not_authorized",
|
|
"Token-2022 proof orchestration did not authorize submission",
|
|
));
|
|
}
|
|
if request.submit && request.plan.policy.dry_run {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"token_2022_execution_dry_run_blocks_submission",
|
|
"Token-2022 plan remains dry-run and cannot be submitted",
|
|
));
|
|
}
|
|
let context_slot = request
|
|
.stateful_preflight
|
|
.context_slot
|
|
.max(request.cryptographic_preflight.context_slot);
|
|
return std::result::Result::Ok(crate::Token2022ExecutionReadinessReport {
|
|
operation_code: request.plan.operation_code.clone(),
|
|
message_hash: request.message_hash.clone(),
|
|
context_slot,
|
|
required_signers,
|
|
send_authorized: request.submit
|
|
&& request.operator_confirmed
|
|
&& request.proof_orchestration.send_authorized
|
|
&& !request.plan.policy.dry_run,
|
|
checks: vec![
|
|
"operation_matches_proof_orchestration".to_string(),
|
|
"simulation_bound_to_exact_message".to_string(),
|
|
"confirmed_stateful_preflight".to_string(),
|
|
"confirmed_cryptographic_preflight".to_string(),
|
|
"all_required_signers_resolved".to_string(),
|
|
"submission_policy_consistent".to_string(),
|
|
],
|
|
});
|
|
}
|
|
|
|
/// Aggregates explicit postconditions without converting unsupported checks into success.
|
|
pub fn summarize_token_2022_postconditions(
|
|
postconditions: &[crate::Token2022ExecutionPostcondition],
|
|
) -> crate::Token2022ExecutionPostconditionStatus {
|
|
if postconditions.iter().any(|item| {
|
|
return item.status == crate::Token2022ExecutionPostconditionStatus::Contradicted;
|
|
}) {
|
|
return crate::Token2022ExecutionPostconditionStatus::Contradicted;
|
|
}
|
|
if postconditions.iter().any(|item| {
|
|
return item.status == crate::Token2022ExecutionPostconditionStatus::Confirmed;
|
|
}) {
|
|
return crate::Token2022ExecutionPostconditionStatus::Confirmed;
|
|
}
|
|
return crate::Token2022ExecutionPostconditionStatus::NotApplicable;
|
|
}
|
|
|
|
fn validate_signers(
|
|
required: &[ks_lib::ExApiRequiredSigner],
|
|
resolved: &[ks_lib::MdPubkey],
|
|
) -> ks_core::Result<std::vec::Vec<ks_lib::MdPubkey>> {
|
|
if required.len() > crate::MAX_TOKEN_2022_EXECUTION_SIGNERS
|
|
|| resolved.len() > crate::MAX_TOKEN_2022_EXECUTION_SIGNERS
|
|
{
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"token_2022_execution_signer_limit_exceeded",
|
|
format!(
|
|
"Token-2022 execution accepts at most {} signers",
|
|
crate::MAX_TOKEN_2022_EXECUTION_SIGNERS
|
|
),
|
|
));
|
|
}
|
|
let mut required_unique =
|
|
std::collections::BTreeMap::<std::string::String, ks_lib::MdPubkey>::new();
|
|
for signer in required {
|
|
required_unique
|
|
.entry(signer.pubkey.0.clone())
|
|
.or_insert_with(|| return signer.pubkey.clone());
|
|
}
|
|
let resolved_set = resolved
|
|
.iter()
|
|
.map(|signer| return signer.0.clone())
|
|
.collect::<std::collections::BTreeSet<std::string::String>>();
|
|
for signer in required_unique.values() {
|
|
if !resolved_set.contains(signer.0.as_str()) {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"token_2022_execution_signer_unresolved",
|
|
format!("Token-2022 required signer {} is unresolved", signer.0),
|
|
));
|
|
}
|
|
}
|
|
return std::result::Result::Ok(required_unique.into_values().collect());
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
fn pubkey(byte: u8) -> ks_lib::MdPubkey {
|
|
return ks_lib::MdPubkey(bs58::encode([byte; 32]).into_string());
|
|
}
|
|
|
|
fn request() -> crate::Token2022ExecutionReadinessRequest {
|
|
let signer = pubkey(1);
|
|
let policy = ks_lib::ExApiExecutionPolicy {
|
|
dry_run: false,
|
|
..std::default::Default::default()
|
|
};
|
|
let plan = ks_lib::ExApiPreparedExecutionPlan {
|
|
executor_name: "kb-lib.executor.spl.token_2022".to_string(),
|
|
executor_version: "0.4.6".to_string(),
|
|
intent_id: "intent".to_string(),
|
|
operation_code: "spl.token_2022.confidential_transfer".to_string(),
|
|
fee_payer: signer.clone(),
|
|
instructions: vec![],
|
|
required_signers: vec![ks_lib::ExApiRequiredSigner {
|
|
pubkey: signer.clone(),
|
|
role: "authority".to_string(),
|
|
}],
|
|
policy,
|
|
requested_spend_lamports: 0,
|
|
requested_compute_unit_price_micro_lamports: std::option::Option::None,
|
|
};
|
|
return crate::Token2022ExecutionReadinessRequest {
|
|
plan,
|
|
message_hash: "message-hash".to_string(),
|
|
simulated_message_hash: "message-hash".to_string(),
|
|
simulated: true,
|
|
simulation_succeeded: true,
|
|
stateful_preflight: crate::Token2022PreflightReport {
|
|
commitment: "confirmed".to_string(),
|
|
context_slot: 100,
|
|
requested_data_bytes: 0,
|
|
accounts: vec![],
|
|
elgamal_registry_validated: false,
|
|
},
|
|
cryptographic_preflight: crate::Token2022CryptographicPreflightReport {
|
|
commitment: "confirmed".to_string(),
|
|
context_slot: 101,
|
|
proof_contexts: vec![],
|
|
},
|
|
proof_orchestration: crate::Token2022ProofOrchestrationReport {
|
|
operation_code: "spl.token_2022.confidential_transfer".to_string(),
|
|
instructions_sysvar_required: true,
|
|
inline_offsets: vec![1],
|
|
context_requirements: vec![],
|
|
simulation_required: true,
|
|
send_authorized: true,
|
|
checks: vec![],
|
|
},
|
|
resolved_signers: vec![signer],
|
|
submit: true,
|
|
operator_confirmed: true,
|
|
};
|
|
}
|
|
|
|
#[test]
|
|
fn exact_message_preflights_and_signers_authorize_submission() {
|
|
let report = super::validate_token_2022_execution_readiness(&request())
|
|
.expect("complete Token-2022 readiness must succeed");
|
|
assert!(report.send_authorized);
|
|
assert_eq!(report.context_slot, 101);
|
|
assert_eq!(report.required_signers.len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn message_mismatch_missing_signer_and_dry_run_fail_closed() {
|
|
let mut mismatched = request();
|
|
mismatched.simulated_message_hash = "other".to_string();
|
|
assert!(super::validate_token_2022_execution_readiness(&mismatched).is_err());
|
|
let mut missing = request();
|
|
missing.resolved_signers.clear();
|
|
assert!(super::validate_token_2022_execution_readiness(&missing).is_err());
|
|
let mut dry_run = request();
|
|
dry_run.plan.policy.dry_run = true;
|
|
assert!(super::validate_token_2022_execution_readiness(&dry_run).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn postcondition_summary_preserves_contradicted_and_not_applicable() {
|
|
let account = pubkey(2);
|
|
let not_applicable = crate::Token2022ExecutionPostcondition {
|
|
role: "mint".to_string(),
|
|
account: account.clone(),
|
|
status: crate::Token2022ExecutionPostconditionStatus::NotApplicable,
|
|
diagnostic: "no supported final-state assertion".to_string(),
|
|
};
|
|
assert_eq!(
|
|
super::summarize_token_2022_postconditions(&[not_applicable]),
|
|
crate::Token2022ExecutionPostconditionStatus::NotApplicable
|
|
);
|
|
let contradicted = crate::Token2022ExecutionPostcondition {
|
|
role: "source".to_string(),
|
|
account,
|
|
status: crate::Token2022ExecutionPostconditionStatus::Contradicted,
|
|
diagnostic: "final extension inventory contradicts the expected effect".to_string(),
|
|
};
|
|
assert_eq!(
|
|
super::summarize_token_2022_postconditions(&[contradicted]),
|
|
crate::Token2022ExecutionPostconditionStatus::Contradicted
|
|
);
|
|
}
|
|
}
|