From 9100fece5bc6ad8a1f20c5e740ca660f4cad5b20 Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Sat, 25 Jul 2026 12:02:05 +0200 Subject: [PATCH] v0.1.0-pre.034 --- kb-pipeline/src/lib.rs | 31 ++ .../src/solana_token2022_correlation.rs | 242 ++++++++++++ .../src/solana_token2022_crypto_preflight.rs | 319 +++++++++++++++ .../solana_token2022_proof_orchestration.rs | 373 ++++++++++++++++++ 4 files changed, 965 insertions(+) create mode 100644 kb-pipeline/src/solana_token2022_correlation.rs create mode 100644 kb-pipeline/src/solana_token2022_crypto_preflight.rs create mode 100644 kb-pipeline/src/solana_token2022_proof_orchestration.rs diff --git a/kb-pipeline/src/lib.rs b/kb-pipeline/src/lib.rs index 0b99782..d6477a2 100644 --- a/kb-pipeline/src/lib.rs +++ b/kb-pipeline/src/lib.rs @@ -13,8 +13,11 @@ mod core_extraction; mod decode_replay; mod plan; mod solana_elgamal_registry_stateful; +mod solana_token2022_correlation; +mod solana_token2022_crypto_preflight; mod solana_token2022_devnet_scenarios; mod solana_token2022_preflight; +mod solana_token2022_proof_orchestration; mod solana_token2022_stateful; mod solana_token2022_validation; @@ -100,6 +103,26 @@ pub use self::solana_elgamal_registry_stateful::materialize_elgamal_registry_acc pub use self::solana_elgamal_registry_stateful::materialize_elgamal_registry_stateful_snapshot; /// Migrated read_elgamal_registry_stateful_snapshot contract. pub use self::solana_elgamal_registry_stateful::read_elgamal_registry_stateful_snapshot; +/// Correlation outcome between one committed instruction fact and one final state snapshot. +pub use self::solana_token2022_correlation::Token2022CorrelationStatus; +/// Deterministic correlation report for one instruction output and one final snapshot. +pub use self::solana_token2022_correlation::Token2022StateCorrelationReport; +/// Correlates one materialized instruction fact with one authoritative Token-2022 snapshot. +pub use self::solana_token2022_correlation::correlate_token2022_instruction_with_snapshot; +/// Maximum number of distinct proof context-state accounts accepted by one preflight. +pub use self::solana_token2022_crypto_preflight::MAX_TOKEN2022_PROOF_CONTEXTS; +/// Token-2022 cryptographic preflight report. +pub use self::solana_token2022_crypto_preflight::Token2022CryptographicPreflightReport; +/// Token-2022 cryptographic preflight request. +pub use self::solana_token2022_crypto_preflight::Token2022CryptographicPreflightRequest; +/// One validated proof context report. +pub use self::solana_token2022_crypto_preflight::Token2022ProofContextReport; +/// One required proof context-state account. +pub use self::solana_token2022_crypto_preflight::Token2022ProofContextRequirement; +/// Encoded proof context metadata bytes. +pub use self::solana_token2022_crypto_preflight::ZK_PROOF_CONTEXT_META_BYTES; +/// Inspects bounded Token-2022 cryptographic proof contexts. +pub use self::solana_token2022_crypto_preflight::inspect_token2022_cryptographic_preflight; /// Stable category of one independent Devnet validation scenario. pub use self::solana_token2022_devnet_scenarios::DevnetSplValidationFamily; /// Current implementation status of one Devnet validation scenario. @@ -122,6 +145,14 @@ pub use self::solana_token2022_preflight::Token2022PreflightRequest; pub use self::solana_token2022_preflight::Token2022PreflightRequirement; /// Migrated inspect_token2022_preflight contract. pub use self::solana_token2022_preflight::inspect_token2022_preflight; +/// Maximum number of proofs accepted for one Token-2022 operation. +pub use self::solana_token2022_proof_orchestration::MAX_TOKEN2022_OPERATION_PROOFS; +/// Token-2022 proof orchestration report. +pub use self::solana_token2022_proof_orchestration::Token2022ProofOrchestrationReport; +/// Token-2022 proof orchestration request. +pub use self::solana_token2022_proof_orchestration::Token2022ProofOrchestrationRequest; +/// Validates mixed inline and context-state proof orchestration. +pub use self::solana_token2022_proof_orchestration::orchestrate_token2022_proofs; /// Migrated MAX_TOKEN2022_STATEFUL_ACCOUNT_BYTES contract. pub use self::solana_token2022_stateful::MAX_TOKEN2022_STATEFUL_ACCOUNT_BYTES; /// Migrated Token2022StatefulContext contract. diff --git a/kb-pipeline/src/solana_token2022_correlation.rs b/kb-pipeline/src/solana_token2022_correlation.rs new file mode 100644 index 0000000..78cecca --- /dev/null +++ b/kb-pipeline/src/solana_token2022_correlation.rs @@ -0,0 +1,242 @@ +// file: kb-pipeline/src/solana_token2022_correlation.rs +// version: 1 + +//! Deterministic Token-2022 instruction-to-state correlation. + +/// Correlation outcome between one committed instruction fact and one final state snapshot. +#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +pub enum Token2022CorrelationStatus { + /// The final snapshot contains the extension expected by the instruction fact. + Confirmed, + /// The final snapshot does not contain the extension expected by the instruction fact. + Contradicted, + /// The instruction output is outside the supported correlation inventory. + NotApplicable, +} + +/// Deterministic correlation report for one instruction output and one final snapshot. +#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct Token2022StateCorrelationReport { + /// Stable caller-provided correlation identity. + pub correlation_key: std::string::String, + /// Canonical account identity checked against the snapshot. + pub account: kb_lib::MdPubkey, + /// Instruction operation or risk fact used for correlation. + pub fact_code: std::string::String, + /// Extension expected from the fact when the fact is supported. + pub expected_extension: std::option::Option, + /// Final correlation outcome. + pub status: Token2022CorrelationStatus, + /// Snapshot context slot. + pub snapshot_slot: u64, + /// Whether the snapshot contains the expected extension. + pub extension_present: std::option::Option, + /// Explicit semantic limitation of this correlation. + pub fact_only: bool, +} + +/// Correlates one materialized instruction fact with one authoritative Token-2022 snapshot. +pub fn correlate_token2022_instruction_with_snapshot( + correlation_key: &str, + account: &kb_lib::MdPubkey, + instruction_output: &kb_lib::MtApiMaterializedOutput, + snapshot: &crate::Token2022StatefulSnapshotBundle, +) -> kb_core::Result { + if correlation_key.trim().is_empty() { + return std::result::Result::Err(kb_core::Error::config( + "Token-2022 correlation key must not be empty", + )); + } + if snapshot.account_key != account.0 { + return std::result::Result::Err(kb_core::Error::new( + "token2022_correlation_account_mismatch", + format!( + "Token-2022 correlation expected account {}, got snapshot {}", + account.0, snapshot.account_key + ), + )); + } + let fact_code = match instruction_output.payload_json.get("riskKind") { + std::option::Option::Some(value) => value.as_str().unwrap_or("").to_string(), + std::option::Option::None => instruction_output + .payload_json + .get("operation") + .and_then(serde_json::Value::as_str) + .unwrap_or("") + .to_string(), + }; + if fact_code.is_empty() { + return std::result::Result::Err(kb_core::Error::new( + "token2022_correlation_fact_missing", + "Token-2022 correlation requires riskKind or operation in the instruction output", + )); + } + let expected_extension = expected_extension(fact_code.as_str()); + let extension_present = expected_extension.map(|extension| { + return snapshot.extension_names.iter().any(|candidate| return candidate == extension); + }); + let status = match extension_present { + std::option::Option::Some(true) => Token2022CorrelationStatus::Confirmed, + std::option::Option::Some(false) => Token2022CorrelationStatus::Contradicted, + std::option::Option::None => Token2022CorrelationStatus::NotApplicable, + }; + return std::result::Result::Ok(Token2022StateCorrelationReport { + correlation_key: correlation_key.to_string(), + account: account.clone(), + fact_code, + expected_extension: expected_extension.map(str::to_string), + status, + snapshot_slot: snapshot.slot, + extension_present, + fact_only: true, + }); +} + +fn expected_extension(fact_code: &str) -> std::option::Option<&'static str> { + return match fact_code { + "default_account_state_configured" + | "initialize_default_account_state" + | "update_default_account_state" => std::option::Option::Some("default_account_state"), + "required_transfer_memos_enabled" + | "required_transfer_memos_disabled" + | "enable_required_transfer_memos" + | "disable_required_transfer_memos" => std::option::Option::Some("memo_transfer"), + "cpi_guard_enabled" | "cpi_guard_disabled" | "enable_cpi_guard" | "disable_cpi_guard" => { + std::option::Option::Some("cpi_guard") + }, + "non_transferable_mint_configured" | "initialize_non_transferable_mint" => { + std::option::Option::Some("non_transferable") + }, + "pausable_mint_configured" + | "mint_activity_paused" + | "mint_activity_resumed" + | "initialize_pausable_config" + | "pause" + | "resume" => std::option::Option::Some("pausable"), + "permissioned_burn_configured" | "initialize_permissioned_burn" => { + std::option::Option::Some("permissioned_burn") + }, + "confidential_credits_enabled" + | "confidential_credits_disabled" + | "non_confidential_credits_enabled" + | "non_confidential_credits_disabled" + | "enable_confidential_credits" + | "disable_confidential_credits" + | "enable_non_confidential_credits" + | "disable_non_confidential_credits" => { + std::option::Option::Some("confidential_transfer_account") + }, + "initialize_transfer_fee_config" + | "set_transfer_fee" + | "withdraw_withheld_tokens_from_mint" + | "harvest_withheld_tokens_to_mint" => std::option::Option::Some("transfer_fee_config"), + "withdraw_withheld_tokens_from_accounts" | "transfer_checked_with_fee" => { + std::option::Option::Some("transfer_fee_amount") + }, + "initialize_confidential_transfer_fee_config" + | "withdraw_confidential_withheld_tokens_from_mint" + | "harvest_confidential_withheld_tokens_to_mint" + | "enable_confidential_harvest_to_mint" + | "disable_confidential_harvest_to_mint" => { + std::option::Option::Some("confidential_transfer_fee_config") + }, + "withdraw_confidential_withheld_tokens_from_accounts" + | "transfer_confidential_tokens_with_fee" => { + std::option::Option::Some("confidential_transfer_fee_amount") + }, + _ => std::option::Option::None, + }; +} + +#[cfg(test)] +mod tests { + fn snapshot(extension_names: &[&str]) -> crate::Token2022StatefulSnapshotBundle { + return crate::Token2022StatefulSnapshotBundle { + account_key: solana_pubkey::Pubkey::new_from_array([7u8; 32]).to_string(), + slot: 42, + state_kind: "mint".to_string(), + extension_names: extension_names + .iter() + .map(|value| return (*value).to_string()) + .collect(), + outputs: std::vec::Vec::new(), + }; + } + + fn output(field: &str, value: &str) -> kb_lib::MtApiMaterializedOutput { + let mut payload = serde_json::Map::new(); + payload.insert(field.to_string(), serde_json::Value::String(value.to_string())); + return kb_lib::MtApiMaterializedOutput { + output_key: "fact:0".to_string(), + family: kb_lib::MdMaterializedEventFamily::Risk, + payload_json: serde_json::Value::Object(payload), + }; + } + + #[test] + fn known_fact_is_confirmed_or_contradicted_by_the_final_extension_inventory() { + let snapshot = snapshot(&["pausable"]); + let account = kb_lib::MdPubkey(snapshot.account_key.clone()); + let confirmed = super::correlate_token2022_instruction_with_snapshot( + "corr:pause", + &account, + &output("riskKind", "mint_activity_paused"), + &snapshot, + ); + assert!(confirmed.is_ok()); + if let std::result::Result::Ok(confirmed) = confirmed { + assert_eq!(confirmed.status, super::Token2022CorrelationStatus::Confirmed); + assert_eq!(confirmed.extension_present, std::option::Option::Some(true)); + } + let contradicted = super::correlate_token2022_instruction_with_snapshot( + "corr:fee", + &account, + &output("operation", "initialize_transfer_fee_config"), + &snapshot, + ); + assert!(contradicted.is_ok()); + if let std::result::Result::Ok(contradicted) = contradicted { + assert_eq!(contradicted.status, super::Token2022CorrelationStatus::Contradicted); + assert_eq!(contradicted.extension_present, std::option::Option::Some(false)); + } + } + + #[test] + fn unsupported_fact_is_explicitly_not_applicable_without_inventing_a_conclusion() { + let snapshot = snapshot(&[]); + let account = kb_lib::MdPubkey(snapshot.account_key.clone()); + let report = super::correlate_token2022_instruction_with_snapshot( + "corr:unknown", + &account, + &output("operation", "unknown_future_operation"), + &snapshot, + ); + assert!(report.is_ok()); + if let std::result::Result::Ok(report) = report { + assert_eq!(report.status, super::Token2022CorrelationStatus::NotApplicable); + assert_eq!(report.extension_present, std::option::Option::None); + assert!(report.fact_only); + } + } + + #[test] + fn correlation_identity_and_account_mismatch_fail_closed() { + let snapshot = snapshot(&["memo_transfer"]); + let account = kb_lib::MdPubkey(snapshot.account_key.clone()); + let empty = super::correlate_token2022_instruction_with_snapshot( + "", + &account, + &output("riskKind", "required_transfer_memos_enabled"), + &snapshot, + ); + assert!(empty.is_err()); + let wrong = kb_lib::MdPubkey(solana_pubkey::Pubkey::new_from_array([8u8; 32]).to_string()); + let mismatch = super::correlate_token2022_instruction_with_snapshot( + "corr:mismatch", + &wrong, + &output("riskKind", "required_transfer_memos_enabled"), + &snapshot, + ); + assert!(mismatch.is_err()); + } +} diff --git a/kb-pipeline/src/solana_token2022_crypto_preflight.rs b/kb-pipeline/src/solana_token2022_crypto_preflight.rs new file mode 100644 index 0000000..0529c86 --- /dev/null +++ b/kb-pipeline/src/solana_token2022_crypto_preflight.rs @@ -0,0 +1,319 @@ +// file: kb-pipeline/src/solana_token2022_crypto_preflight.rs +// version: 2 + +//! Bounded cryptographic preflight for Token-2022 proof context-state accounts. + +/// Maximum number of distinct proof context-state accounts accepted by one request. +pub const MAX_TOKEN2022_PROOF_CONTEXTS: usize = 8; +/// Generic metadata bytes retained by every ZK ElGamal proof context-state account. +pub const ZK_PROOF_CONTEXT_META_BYTES: usize = 33; + +/// One exact pre-verified proof context-state requirement. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Token2022ProofContextRequirement { + /// Stable semantic role used in diagnostics. + pub role: std::string::String, + /// Canonical proof context-state account. + pub account: kb_lib::MdPubkey, + /// Exact proof kind expected by the Token-2022 builder. + pub proof_type: kb_lib::ExSolanaCoreZkElGamalProofType, + /// Optional authority retained by the context-state account. + pub expected_authority: std::option::Option, +} + +/// One bounded cryptographic preflight request. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Token2022CryptographicPreflightRequest { + /// Endpoint role used by every account read. + pub query_role: std::string::String, + /// Optional minimum RPC context slot. + pub min_context_slot: std::option::Option, + /// Ordered proof context-state requirements. + pub proof_contexts: std::vec::Vec, +} + +/// One validated proof context-state result. +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct Token2022ProofContextReport { + /// Stable semantic role. + pub role: std::string::String, + /// Canonical proof context-state account. + pub account: kb_lib::MdPubkey, + /// Official one-byte proof discriminator. + pub proof_discriminator: u8, + /// Exact context-state size for the proof type. + pub context_state_bytes: usize, + /// Context slot returned by the endpoint. + pub context_slot: u64, + /// Ordered successful checks. + pub checks: std::vec::Vec, +} + +/// Complete cryptographic preflight report. +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct Token2022CryptographicPreflightReport { + /// Commitment used for all reads. + pub commitment: std::string::String, + /// Highest context slot observed across all reads. + pub context_slot: u64, + /// Ordered validated proof contexts. + pub proof_contexts: std::vec::Vec, +} + +/// Reads and validates all pre-verified proof context-state accounts required by one operation. +pub async fn inspect_token2022_cryptographic_preflight( + pool: &kb_onchain_transport::HttpEndpointPool, + request: &crate::Token2022CryptographicPreflightRequest, +) -> kb_core::Result { + let requirements = match validate_proof_context_requirements(request) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let mut context_slot = request.min_context_slot.unwrap_or(0); + let mut reports = std::vec::Vec::with_capacity(requirements.len()); + for requirement in requirements { + let expected_size = requirement.proof_type.context_state_size(); + let config = match kb_onchain_transport::GetAccountInfoConfig::new_with_data( + kb_onchain_transport::RpcCommitmentLevel::Confirmed, + request.min_context_slot, + expected_size, + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let result = match pool + .get_account_info_for_role(request.query_role.as_str(), &requirement.account, &config) + .await + { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let report = match validate_proof_context_account(&requirement, &result) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + context_slot = context_slot.max(report.context_slot); + reports.push(report); + } + return std::result::Result::Ok(crate::Token2022CryptographicPreflightReport { + commitment: "confirmed".to_string(), + context_slot, + proof_contexts: reports, + }); +} + +fn validate_proof_context_requirements( + request: &crate::Token2022CryptographicPreflightRequest, +) -> kb_core::Result> { + if request.query_role.trim().is_empty() { + return std::result::Result::Err(kb_core::Error::config( + "Token-2022 cryptographic preflight query_role must not be empty", + )); + } + if request.proof_contexts.len() > crate::MAX_TOKEN2022_PROOF_CONTEXTS { + return std::result::Result::Err(kb_core::Error::new( + "token2022_proof_context_limit_exceeded", + format!( + "Token-2022 cryptographic preflight accepts at most {} proof contexts", + crate::MAX_TOKEN2022_PROOF_CONTEXTS + ), + )); + } + let mut indexes = std::collections::BTreeMap::::new(); + let mut unique = std::vec::Vec::::new(); + for requirement in &request.proof_contexts { + if requirement.role.trim().is_empty() { + return std::result::Result::Err(kb_core::Error::config( + "Token-2022 proof context role must not be empty", + )); + } + if let std::option::Option::Some(index) = indexes.get(requirement.account.0.as_str()) { + if &unique[*index] != requirement { + return std::result::Result::Err(kb_core::Error::new( + "token2022_proof_context_conflicting_duplicate", + format!( + "Token-2022 proof context {} has conflicting requirements", + requirement.account.0 + ), + )); + } + continue; + } + indexes.insert(requirement.account.0.clone(), unique.len()); + unique.push(requirement.clone()); + } + return std::result::Result::Ok(unique); +} + +fn validate_proof_context_account( + requirement: &crate::Token2022ProofContextRequirement, + result: &kb_onchain_transport::AccountInfoResult, +) -> kb_core::Result { + let account = match result.account.as_ref() { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(kb_core::Error::new( + "token2022_proof_context_missing", + format!( + "Token-2022 proof context account {} does not exist", + requirement.account.0 + ), + )); + }, + }; + if account.owner.0 != kb_program_ids::ZK_ELGAMAL_PROOF_PROGRAM_ID { + return std::result::Result::Err(kb_core::Error::new( + "token2022_proof_context_owner_mismatch", + format!( + "Token-2022 proof context owner must be {}, got {}", + kb_program_ids::ZK_ELGAMAL_PROOF_PROGRAM_ID, + account.owner.0 + ), + )); + } + if account.executable { + return std::result::Result::Err(kb_core::Error::new( + "token2022_proof_context_executable", + "Token-2022 proof context account must not be executable", + )); + } + let expected_size = requirement.proof_type.context_state_size(); + if account.space != expected_size as u64 || account.data.len() != expected_size { + return std::result::Result::Err(kb_core::Error::new( + "token2022_proof_context_size_mismatch", + format!( + "Token-2022 proof context {} must contain exactly {expected_size} bytes, got space {} and data {}", + requirement.account.0, + account.space, + account.data.len() + ), + )); + } + if account.data.len() < crate::ZK_PROOF_CONTEXT_META_BYTES { + return std::result::Result::Err(kb_core::Error::invalid_state( + "Token-2022 proof context is shorter than its generic metadata", + )); + } + let discriminator = account.data[32]; + if discriminator != requirement.proof_type.discriminator() { + return std::result::Result::Err(kb_core::Error::new( + "token2022_proof_context_type_mismatch", + format!( + "Token-2022 proof context {} discriminator {} does not match expected {}", + requirement.account.0, + discriminator, + requirement.proof_type.discriminator() + ), + )); + } + let mut checks = std::vec![ + "zk_program_owner".to_string(), + "not_executable".to_string(), + "exact_context_state_size".to_string(), + "proof_type_discriminator".to_string(), + ]; + if let std::option::Option::Some(expected_authority) = requirement.expected_authority.as_ref() { + let retained_authority = bs58::encode(&account.data[..32]).into_string(); + if retained_authority != expected_authority.0 { + return std::result::Result::Err(kb_core::Error::new( + "token2022_proof_context_authority_mismatch", + format!( + "Token-2022 proof context {} retains authority {}, expected {}", + requirement.account.0, retained_authority, expected_authority.0 + ), + )); + } + checks.push("retained_authority".to_string()); + } + return std::result::Result::Ok(crate::Token2022ProofContextReport { + role: requirement.role.clone(), + account: requirement.account.clone(), + proof_discriminator: discriminator, + context_state_bytes: expected_size, + context_slot: result.context.slot, + checks, + }); +} + +#[cfg(test)] +mod tests { + fn pubkey(byte: u8) -> kb_lib::MdPubkey { + return kb_lib::MdPubkey(bs58::encode([byte; 32]).into_string()); + } + + fn requirement( + account: kb_lib::MdPubkey, + proof_type: kb_lib::ExSolanaCoreZkElGamalProofType, + ) -> crate::Token2022ProofContextRequirement { + return crate::Token2022ProofContextRequirement { + role: "equality_proof".to_string(), + account, + proof_type, + expected_authority: std::option::Option::Some(pubkey(9)), + }; + } + + fn account_result( + proof_type: kb_lib::ExSolanaCoreZkElGamalProofType, + ) -> kb_onchain_transport::AccountInfoResult { + let mut data = std::vec![0u8; proof_type.context_state_size()]; + data[..32].copy_from_slice(&[9u8; 32]); + data[32] = proof_type.discriminator(); + return kb_onchain_transport::AccountInfoResult { + context: kb_onchain_transport::RpcResponseContext { + slot: 77, + api_version: std::option::Option::None, + }, + account: std::option::Option::Some(kb_onchain_transport::AccountInfoValue { + lamports: 1, + owner: kb_lib::MdProgramId(kb_program_ids::ZK_ELGAMAL_PROOF_PROGRAM_ID.to_string()), + executable: false, + rent_epoch: 0, + space: data.len() as u64, + data, + }), + }; + } + + #[test] + fn exact_duplicates_are_deduplicated_and_conflicts_fail_closed() { + let item = requirement( + pubkey(1), + kb_lib::ExSolanaCoreZkElGamalProofType::CiphertextCiphertextEquality, + ); + let request = crate::Token2022CryptographicPreflightRequest { + query_role: "query".to_string(), + min_context_slot: std::option::Option::Some(7), + proof_contexts: std::vec![item.clone(), item.clone()], + }; + let unique = super::validate_proof_context_requirements(&request); + assert_eq!(unique.as_ref().map(std::vec::Vec::len), std::result::Result::Ok(1)); + let conflict = crate::Token2022CryptographicPreflightRequest { + query_role: "query".to_string(), + min_context_slot: std::option::Option::None, + proof_contexts: std::vec![ + item, + requirement( + pubkey(1), + kb_lib::ExSolanaCoreZkElGamalProofType::BatchedRangeProofU128, + ), + ], + }; + assert!(super::validate_proof_context_requirements(&conflict).is_err()); + } + + #[test] + fn proof_context_validation_checks_owner_size_type_and_authority() { + let proof_type = kb_lib::ExSolanaCoreZkElGamalProofType::CiphertextCiphertextEquality; + let context_requirement = requirement(pubkey(1), proof_type); + let result = account_result(proof_type); + let report = super::validate_proof_context_account(&context_requirement, &result); + assert_eq!( + report.as_ref().map(|value| return value.checks.len()), + std::result::Result::Ok(5) + ); + let wrong_type = + requirement(pubkey(1), kb_lib::ExSolanaCoreZkElGamalProofType::BatchedRangeProofU128); + assert!(super::validate_proof_context_account(&wrong_type, &result,).is_err()); + } +} diff --git a/kb-pipeline/src/solana_token2022_proof_orchestration.rs b/kb-pipeline/src/solana_token2022_proof_orchestration.rs new file mode 100644 index 0000000..114a336 --- /dev/null +++ b/kb-pipeline/src/solana_token2022_proof_orchestration.rs @@ -0,0 +1,373 @@ +// file: kb-pipeline/src/solana_token2022_proof_orchestration.rs +// version: 2 + +//! Deterministic orchestration contract for mixed Token-2022 proof locations. + +/// Maximum proof references accepted by one confidential Token-2022 operation. +pub const MAX_TOKEN2022_OPERATION_PROOFS: usize = 5; + +/// One bounded proof-orchestration request prepared before transaction assembly. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Token2022ProofOrchestrationRequest { + /// Stable executor operation code. + pub operation_code: std::string::String, + /// Exact ordered proof references required by the operation. + pub proofs: std::vec::Vec, + /// Optional authority expected on every context-state account. + pub expected_context_authority: std::option::Option, + /// Maximum compute-unit limit accepted for the future transaction. + pub compute_unit_limit: u32, + /// Maximum total fee accepted for the future transaction. + pub max_fee_lamports: u64, + /// Whether a future submission was explicitly requested. + pub submit: bool, + /// Whether the operator explicitly confirmed the future submission. + pub operator_confirmed: bool, +} + +/// One deterministic mixed-proof orchestration result. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Token2022ProofOrchestrationReport { + /// Stable executor operation code. + pub operation_code: std::string::String, + /// Whether the instructions sysvar must be present. + pub instructions_sysvar_required: bool, + /// Ordered non-zero inline offsets. + pub inline_offsets: std::vec::Vec, + /// Ordered validated context-state requirements. + pub context_requirements: std::vec::Vec, + /// Simulation is always mandatory. + pub simulation_required: bool, + /// Whether the future send path is authorized by request-local policy. + pub send_authorized: bool, + /// Ordered successful checks. + pub checks: std::vec::Vec, +} + +/// Validates mixed inline/context-state proof orchestration before transaction assembly. +pub fn orchestrate_token2022_proofs( + request: &crate::Token2022ProofOrchestrationRequest, + cryptographic_preflight: &crate::Token2022CryptographicPreflightReport, +) -> kb_core::Result { + if request.operation_code.trim().is_empty() { + return std::result::Result::Err(kb_core::Error::config( + "Token-2022 proof orchestration operation_code must not be empty", + )); + } + if request.proofs.len() > crate::MAX_TOKEN2022_OPERATION_PROOFS { + return std::result::Result::Err(kb_core::Error::new( + "token2022_operation_proof_limit_exceeded", + format!( + "Token-2022 proof orchestration accepts at most {} proofs", + crate::MAX_TOKEN2022_OPERATION_PROOFS + ), + )); + } + if request.compute_unit_limit == 0 || request.max_fee_lamports == 0 { + return std::result::Result::Err(kb_core::Error::config( + "Token-2022 proof orchestration requires non-zero compute and fee ceilings", + )); + } + if request.submit && !request.operator_confirmed { + return std::result::Result::Err(kb_core::Error::new( + "token2022_submission_confirmation_required", + "Token-2022 submission requires explicit operator confirmation", + )); + } + let mut kinds = std::collections::BTreeSet::::new(); + let mut offsets = std::collections::BTreeSet::::new(); + let mut context_accounts = std::collections::BTreeSet::::new(); + let mut inline_offsets = std::vec::Vec::::new(); + let mut context_requirements = std::vec::Vec::::new(); + for proof in &request.proofs { + let kind_code = proof_kind_code(proof.kind).to_string(); + if !kinds.insert(kind_code.clone()) { + return std::result::Result::Err(kb_core::Error::new( + "token2022_duplicate_proof_kind", + format!( + "Token-2022 operation {} contains duplicate proof kind {kind_code}", + request.operation_code + ), + )); + } + if let std::result::Result::Err(error) = proof.location.validate() { + return std::result::Result::Err(kb_core::Error::invalid_state(error)); + } + match &proof.location { + kb_lib::ExSplTokenConfidentialProofLocation::InstructionOffset { offset } => { + if !offsets.insert(*offset) { + return std::result::Result::Err(kb_core::Error::new( + "token2022_duplicate_inline_proof_offset", + format!( + "Token-2022 operation {} reuses inline proof offset {offset}", + request.operation_code + ), + )); + } + inline_offsets.push(*offset); + }, + kb_lib::ExSplTokenConfidentialProofLocation::ContextStateAccount { account } => { + if !context_accounts.insert(account.0.clone()) { + return std::result::Result::Err(kb_core::Error::new( + "token2022_duplicate_context_state_account", + format!( + "Token-2022 operation {} reuses context-state account {}", + request.operation_code, account.0 + ), + )); + } + context_requirements.push(crate::Token2022ProofContextRequirement { + role: kind_code, + account: account.clone(), + proof_type: proof_kind_to_zk_type(proof.kind), + expected_authority: request.expected_context_authority.clone(), + }); + }, + } + } + if let std::result::Result::Err(error) = + validate_context_reports(context_requirements.as_slice(), cryptographic_preflight) + { + return std::result::Result::Err(error); + } + return std::result::Result::Ok(crate::Token2022ProofOrchestrationReport { + operation_code: request.operation_code.clone(), + instructions_sysvar_required: !inline_offsets.is_empty(), + inline_offsets, + context_requirements, + simulation_required: true, + send_authorized: request.submit && request.operator_confirmed, + checks: vec![ + "ordered_unique_proof_kinds".to_string(), + "non_zero_unique_inline_offsets".to_string(), + "unique_context_state_accounts".to_string(), + "context_preflight_matches_operation".to_string(), + "simulation_required".to_string(), + "bounded_compute_and_fee".to_string(), + ], + }); +} + +fn validate_context_reports( + requirements: &[crate::Token2022ProofContextRequirement], + report: &crate::Token2022CryptographicPreflightReport, +) -> kb_core::Result<()> { + if requirements.len() != report.proof_contexts.len() { + return std::result::Result::Err(kb_core::Error::new( + "token2022_context_preflight_count_mismatch", + format!( + "Token-2022 operation requires {} context states, preflight contains {}", + requirements.len(), + report.proof_contexts.len() + ), + )); + } + for (requirement, observed) in requirements.iter().zip(report.proof_contexts.iter()) { + if requirement.account != observed.account + || requirement.proof_type.discriminator() != observed.proof_discriminator + { + return std::result::Result::Err(kb_core::Error::new( + "token2022_context_preflight_mismatch", + format!( + "Token-2022 proof context {} does not match the ordered operation requirement", + observed.account.0 + ), + )); + } + } + return std::result::Result::Ok(()); +} + +fn proof_kind_code(kind: kb_lib::ExSplTokenConfidentialProofKind) -> &'static str { + return match kind { + kb_lib::ExSplTokenConfidentialProofKind::PubkeyValidity => "pubkey_validity", + kb_lib::ExSplTokenConfidentialProofKind::ZeroCiphertext => "zero_ciphertext", + kb_lib::ExSplTokenConfidentialProofKind::CiphertextCommitmentEquality => { + "ciphertext_commitment_equality" + }, + kb_lib::ExSplTokenConfidentialProofKind::CiphertextCiphertextEquality => { + "ciphertext_ciphertext_equality" + }, + kb_lib::ExSplTokenConfidentialProofKind::BatchedGroupedCiphertext3HandlesValidity => { + "batched_grouped_ciphertext_3_handles_validity" + }, + kb_lib::ExSplTokenConfidentialProofKind::BatchedGroupedCiphertext2HandlesValidity => { + "batched_grouped_ciphertext_2_handles_validity" + }, + kb_lib::ExSplTokenConfidentialProofKind::PercentageWithFee => "percentage_with_fee", + kb_lib::ExSplTokenConfidentialProofKind::BatchedRangeProofU64 => "batched_range_proof_u64", + kb_lib::ExSplTokenConfidentialProofKind::BatchedRangeProofU128 => { + "batched_range_proof_u128" + }, + kb_lib::ExSplTokenConfidentialProofKind::BatchedRangeProofU256 => { + "batched_range_proof_u256" + }, + }; +} + +fn proof_kind_to_zk_type( + kind: kb_lib::ExSplTokenConfidentialProofKind, +) -> kb_lib::ExSolanaCoreZkElGamalProofType { + return match kind { + kb_lib::ExSplTokenConfidentialProofKind::PubkeyValidity => { + kb_lib::ExSolanaCoreZkElGamalProofType::PubkeyValidity + }, + kb_lib::ExSplTokenConfidentialProofKind::ZeroCiphertext => { + kb_lib::ExSolanaCoreZkElGamalProofType::ZeroCiphertext + }, + kb_lib::ExSplTokenConfidentialProofKind::CiphertextCommitmentEquality => { + kb_lib::ExSolanaCoreZkElGamalProofType::CiphertextCommitmentEquality + }, + kb_lib::ExSplTokenConfidentialProofKind::CiphertextCiphertextEquality => { + kb_lib::ExSolanaCoreZkElGamalProofType::CiphertextCiphertextEquality + }, + kb_lib::ExSplTokenConfidentialProofKind::BatchedGroupedCiphertext3HandlesValidity => { + kb_lib::ExSolanaCoreZkElGamalProofType::BatchedGroupedCiphertext3HandlesValidity + }, + kb_lib::ExSplTokenConfidentialProofKind::BatchedGroupedCiphertext2HandlesValidity => { + kb_lib::ExSolanaCoreZkElGamalProofType::BatchedGroupedCiphertext2HandlesValidity + }, + kb_lib::ExSplTokenConfidentialProofKind::PercentageWithFee => { + kb_lib::ExSolanaCoreZkElGamalProofType::PercentageWithCap + }, + kb_lib::ExSplTokenConfidentialProofKind::BatchedRangeProofU64 => { + kb_lib::ExSolanaCoreZkElGamalProofType::BatchedRangeProofU64 + }, + kb_lib::ExSplTokenConfidentialProofKind::BatchedRangeProofU128 => { + kb_lib::ExSolanaCoreZkElGamalProofType::BatchedRangeProofU128 + }, + kb_lib::ExSplTokenConfidentialProofKind::BatchedRangeProofU256 => { + kb_lib::ExSolanaCoreZkElGamalProofType::BatchedRangeProofU256 + }, + }; +} + +#[cfg(test)] +mod tests { + fn pubkey(byte: u8) -> kb_lib::MdPubkey { + return kb_lib::MdPubkey(bs58::encode([byte; 32]).into_string()); + } + + fn context_report( + account: kb_lib::MdPubkey, + proof_type: kb_lib::ExSolanaCoreZkElGamalProofType, + ) -> crate::Token2022ProofContextReport { + return crate::Token2022ProofContextReport { + role: "proof".to_string(), + account, + proof_discriminator: proof_type.discriminator(), + context_state_bytes: proof_type.context_state_size(), + context_slot: 100, + checks: vec!["validated".to_string()], + }; + } + + #[test] + fn mixed_proofs_require_sysvar_and_preserve_ordered_contexts() { + let context = pubkey(1); + let request = crate::Token2022ProofOrchestrationRequest { + operation_code: "spl_token2022.confidential_transfer".to_string(), + proofs: vec![ + kb_lib::ExSplTokenConfidentialProofReference { + kind: kb_lib::ExSplTokenConfidentialProofKind::CiphertextCommitmentEquality, + location: kb_lib::ExSplTokenConfidentialProofLocation::InstructionOffset { + offset: 1, + }, + }, + kb_lib::ExSplTokenConfidentialProofReference { + kind: kb_lib::ExSplTokenConfidentialProofKind::BatchedRangeProofU128, + location: kb_lib::ExSplTokenConfidentialProofLocation::ContextStateAccount { + account: context.clone(), + }, + }, + ], + expected_context_authority: std::option::Option::None, + compute_unit_limit: 400_000, + max_fee_lamports: 50_000, + submit: false, + operator_confirmed: false, + }; + let preflight = crate::Token2022CryptographicPreflightReport { + commitment: "confirmed".to_string(), + context_slot: 100, + proof_contexts: vec![context_report( + context, + kb_lib::ExSolanaCoreZkElGamalProofType::BatchedRangeProofU128, + )], + }; + let report = crate::orchestrate_token2022_proofs(&request, &preflight) + .expect("mixed proof orchestration must succeed"); + assert!(report.instructions_sysvar_required); + assert_eq!(report.inline_offsets, vec![1]); + assert_eq!(report.context_requirements.len(), 1); + assert!(report.simulation_required); + assert!(!report.send_authorized); + } + + #[test] + fn context_only_proofs_do_not_require_instructions_sysvar() { + let context = pubkey(2); + let request = crate::Token2022ProofOrchestrationRequest { + operation_code: "spl_token2022.empty_confidential_account".to_string(), + proofs: vec![kb_lib::ExSplTokenConfidentialProofReference { + kind: kb_lib::ExSplTokenConfidentialProofKind::ZeroCiphertext, + location: kb_lib::ExSplTokenConfidentialProofLocation::ContextStateAccount { + account: context.clone(), + }, + }], + expected_context_authority: std::option::Option::None, + compute_unit_limit: 200_000, + max_fee_lamports: 20_000, + submit: true, + operator_confirmed: true, + }; + let preflight = crate::Token2022CryptographicPreflightReport { + commitment: "confirmed".to_string(), + context_slot: 100, + proof_contexts: vec![context_report( + context, + kb_lib::ExSolanaCoreZkElGamalProofType::ZeroCiphertext, + )], + }; + let report = crate::orchestrate_token2022_proofs(&request, &preflight) + .expect("context-only orchestration must succeed"); + assert!(!report.instructions_sysvar_required); + assert!(report.send_authorized); + } + + #[test] + fn duplicate_offsets_contexts_and_unconfirmed_submission_fail_closed() { + let duplicate_offset = crate::Token2022ProofOrchestrationRequest { + operation_code: "operation".to_string(), + proofs: vec![ + kb_lib::ExSplTokenConfidentialProofReference { + kind: kb_lib::ExSplTokenConfidentialProofKind::ZeroCiphertext, + location: kb_lib::ExSplTokenConfidentialProofLocation::InstructionOffset { + offset: 1, + }, + }, + kb_lib::ExSplTokenConfidentialProofReference { + kind: kb_lib::ExSplTokenConfidentialProofKind::BatchedRangeProofU64, + location: kb_lib::ExSplTokenConfidentialProofLocation::InstructionOffset { + offset: 1, + }, + }, + ], + expected_context_authority: std::option::Option::None, + compute_unit_limit: 1, + max_fee_lamports: 1, + submit: false, + operator_confirmed: false, + }; + let empty = crate::Token2022CryptographicPreflightReport { + commitment: "confirmed".to_string(), + context_slot: 0, + proof_contexts: vec![], + }; + assert!(crate::orchestrate_token2022_proofs(&duplicate_offset, &empty).is_err()); + let mut unconfirmed = duplicate_offset; + unconfirmed.proofs.clear(); + unconfirmed.submit = true; + assert!(crate::orchestrate_token2022_proofs(&unconfirmed, &empty).is_err()); + } +}