1624 lines
62 KiB
Rust
1624 lines
62 KiB
Rust
// file: kb-pipeline/src/solana_stateful.rs
|
|
// version: 2
|
|
|
|
//! Stateful Localnet and Devnet readiness checks for native Solana execution plans.
|
|
|
|
use std::str::FromStr; // rust-rules: trait-import
|
|
|
|
const COMPLETE_ACCOUNT_DATA_LIMIT: usize = 65_536;
|
|
const ALT_META_SIZE: usize = 56;
|
|
const ALT_MAX_ADDRESSES: usize = 256;
|
|
const FEATURE_ACCOUNT_SIZE: u64 = 9;
|
|
const SLASHING_REPORT_HEADER_SIZE: usize = 114;
|
|
const MAX_SHRED_SIZE: usize = 1_232;
|
|
const ZK_CONTEXT_META_SIZE: usize = 33;
|
|
|
|
/// Stateful readiness outcome for one native Solana operation.
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum SolanaCoreStatefulReadinessStatus {
|
|
/// The operation has no stateful preflight contract in this layer.
|
|
NotRequired,
|
|
/// Every stateful preflight check passed.
|
|
Ready,
|
|
/// At least one stateful preflight check failed.
|
|
Blocked,
|
|
}
|
|
|
|
/// One machine-readable stateful readiness check.
|
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
|
pub struct SolanaCoreStatefulCheck {
|
|
/// Stable diagnostic code.
|
|
pub code: std::string::String,
|
|
/// Whether the check passed.
|
|
pub passed: bool,
|
|
/// Operator-readable explanation.
|
|
pub message: std::string::String,
|
|
}
|
|
|
|
/// One contextual fact measured during stateful readiness inspection.
|
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
|
pub struct SolanaCoreStatefulFact {
|
|
/// Stable fact key.
|
|
pub key: std::string::String,
|
|
/// String representation of the measured value.
|
|
pub value: std::string::String,
|
|
}
|
|
|
|
/// Complete request for one native Solana stateful readiness inspection.
|
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
|
pub struct SolanaCoreStatefulReadinessRequest {
|
|
/// HTTP endpoint role used for all state reads.
|
|
pub query_role: std::string::String,
|
|
/// Expected Localnet or Devnet cluster.
|
|
pub cluster: kb_lib::ExApiExecutionCluster,
|
|
/// Native operation whose external state must be inspected.
|
|
pub operation: kb_lib::ExSolanaCoreOperation,
|
|
}
|
|
|
|
/// Complete stateful readiness report produced before simulation.
|
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
|
pub struct SolanaCoreStatefulReadinessReport {
|
|
/// Expected cluster.
|
|
pub cluster: kb_lib::ExApiExecutionCluster,
|
|
/// Stable native operation code.
|
|
pub operation_code: std::string::String,
|
|
/// Aggregate stateful readiness status.
|
|
pub status: crate::SolanaCoreStatefulReadinessStatus,
|
|
/// Highest contextual slot observed while reading accounts.
|
|
pub context_slot: std::option::Option<u64>,
|
|
/// Ordered stateful checks.
|
|
pub checks: std::vec::Vec<crate::SolanaCoreStatefulCheck>,
|
|
/// Ordered measured facts.
|
|
pub facts: std::vec::Vec<crate::SolanaCoreStatefulFact>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
struct AddressLookupTableState {
|
|
deactivation_slot: u64,
|
|
authority: std::option::Option<std::string::String>,
|
|
address_count: usize,
|
|
}
|
|
|
|
/// Inspects Localnet or Devnet state required before simulating one native operation.
|
|
pub async fn inspect_solana_core_stateful_readiness(
|
|
pool: &kb_onchain_transport::HttpEndpointPool,
|
|
request: &crate::SolanaCoreStatefulReadinessRequest,
|
|
) -> kb_core::Result<crate::SolanaCoreStatefulReadinessReport> {
|
|
if request.query_role.trim().is_empty() {
|
|
return std::result::Result::Err(kb_core::Error::config(
|
|
"stateful execution readiness query_role must not be empty",
|
|
));
|
|
}
|
|
match request.cluster {
|
|
kb_lib::ExApiExecutionCluster::Localnet | kb_lib::ExApiExecutionCluster::Devnet => {},
|
|
kb_lib::ExApiExecutionCluster::Testnet | kb_lib::ExApiExecutionCluster::Mainnet => {
|
|
return std::result::Result::Err(kb_core::Error::new(
|
|
"execution_stateful_cluster_unsupported",
|
|
"stateful native execution readiness is restricted to Localnet and Devnet",
|
|
));
|
|
},
|
|
}
|
|
let genesis = match pool.get_genesis_hash_for_role(request.query_role.as_str()).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
match validate_cluster(request.cluster, &genesis) {
|
|
std::result::Result::Ok(()) => {},
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
}
|
|
let mut report = new_report(request);
|
|
push_fact(&mut report, "genesis_hash", genesis.genesis_hash.as_str());
|
|
match &request.operation {
|
|
kb_lib::ExSolanaCoreOperation::AddressLookupTableCreate {
|
|
authority,
|
|
payer,
|
|
recent_slot,
|
|
} => {
|
|
match inspect_alt_create(pool, request, &mut report, authority, payer, *recent_slot)
|
|
.await
|
|
{
|
|
std::result::Result::Ok(()) => {},
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(error);
|
|
},
|
|
}
|
|
},
|
|
kb_lib::ExSolanaCoreOperation::AddressLookupTableExtend {
|
|
lookup_table,
|
|
authority,
|
|
payer,
|
|
new_addresses,
|
|
} => {
|
|
match inspect_alt_existing(
|
|
pool,
|
|
request,
|
|
&mut report,
|
|
lookup_table,
|
|
authority,
|
|
AltAction::Extend {
|
|
payer_present: payer.is_some(),
|
|
added_addresses: new_addresses.len(),
|
|
},
|
|
)
|
|
.await
|
|
{
|
|
std::result::Result::Ok(()) => {},
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(error);
|
|
},
|
|
}
|
|
},
|
|
kb_lib::ExSolanaCoreOperation::AddressLookupTableFreeze { lookup_table, authority } => {
|
|
match inspect_alt_existing(
|
|
pool,
|
|
request,
|
|
&mut report,
|
|
lookup_table,
|
|
authority,
|
|
AltAction::Freeze,
|
|
)
|
|
.await
|
|
{
|
|
std::result::Result::Ok(()) => {},
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(error);
|
|
},
|
|
}
|
|
},
|
|
kb_lib::ExSolanaCoreOperation::AddressLookupTableDeactivate { lookup_table, authority } => {
|
|
match inspect_alt_existing(
|
|
pool,
|
|
request,
|
|
&mut report,
|
|
lookup_table,
|
|
authority,
|
|
AltAction::Deactivate,
|
|
)
|
|
.await
|
|
{
|
|
std::result::Result::Ok(()) => {},
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(error);
|
|
},
|
|
}
|
|
},
|
|
kb_lib::ExSolanaCoreOperation::AddressLookupTableClose {
|
|
lookup_table, authority, ..
|
|
} => {
|
|
match inspect_alt_existing(
|
|
pool,
|
|
request,
|
|
&mut report,
|
|
lookup_table,
|
|
authority,
|
|
AltAction::Close,
|
|
)
|
|
.await
|
|
{
|
|
std::result::Result::Ok(()) => {},
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(error);
|
|
},
|
|
}
|
|
},
|
|
kb_lib::ExSolanaCoreOperation::ConfigCreateAccount {
|
|
config_account,
|
|
lamports,
|
|
max_config_data_space,
|
|
max_keys,
|
|
..
|
|
} => {
|
|
match inspect_config_create(
|
|
pool,
|
|
request,
|
|
&mut report,
|
|
config_account,
|
|
*lamports,
|
|
*max_config_data_space,
|
|
max_keys.len(),
|
|
)
|
|
.await
|
|
{
|
|
std::result::Result::Ok(()) => {},
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(error);
|
|
},
|
|
}
|
|
},
|
|
kb_lib::ExSolanaCoreOperation::ConfigStore { config_account, keys, data, .. } => {
|
|
match inspect_config_store(
|
|
pool,
|
|
request,
|
|
&mut report,
|
|
config_account,
|
|
keys.len(),
|
|
data.len(),
|
|
)
|
|
.await
|
|
{
|
|
std::result::Result::Ok(()) => {},
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(error);
|
|
},
|
|
}
|
|
},
|
|
kb_lib::ExSolanaCoreOperation::FeatureActivate { feature_account, lamports, .. } => {
|
|
match inspect_feature_activate(pool, request, &mut report, feature_account, *lamports)
|
|
.await
|
|
{
|
|
std::result::Result::Ok(()) => {},
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(error);
|
|
},
|
|
}
|
|
},
|
|
kb_lib::ExSolanaCoreOperation::FeatureRevokePendingActivation { feature_account } => {
|
|
match inspect_feature_revoke(pool, request, &mut report, feature_account).await {
|
|
std::result::Result::Ok(()) => {},
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(error);
|
|
},
|
|
}
|
|
},
|
|
kb_lib::ExSolanaCoreOperation::SlashingSubmitDuplicateBlockProof {
|
|
proof_account,
|
|
report_account,
|
|
report_lamports,
|
|
proof_offset,
|
|
slot,
|
|
..
|
|
} => {
|
|
match inspect_slashing_submit(
|
|
pool,
|
|
request,
|
|
&mut report,
|
|
proof_account,
|
|
report_account,
|
|
*report_lamports,
|
|
*proof_offset,
|
|
*slot,
|
|
)
|
|
.await
|
|
{
|
|
std::result::Result::Ok(()) => {},
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(error);
|
|
},
|
|
}
|
|
},
|
|
kb_lib::ExSolanaCoreOperation::SlashingCloseViolationReport {
|
|
report_account,
|
|
destination,
|
|
} => {
|
|
match inspect_slashing_close(pool, request, &mut report, report_account, destination)
|
|
.await
|
|
{
|
|
std::result::Result::Ok(()) => {},
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(error);
|
|
},
|
|
}
|
|
},
|
|
kb_lib::ExSolanaCoreOperation::ZkElGamalVerifyInline {
|
|
proof_type, context_state, ..
|
|
} => {
|
|
match inspect_zk_verify(
|
|
pool,
|
|
request,
|
|
&mut report,
|
|
*proof_type,
|
|
std::option::Option::None,
|
|
context_state.as_ref(),
|
|
)
|
|
.await
|
|
{
|
|
std::result::Result::Ok(()) => {},
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(error);
|
|
},
|
|
}
|
|
},
|
|
kb_lib::ExSolanaCoreOperation::ZkElGamalVerifyFromAccount {
|
|
proof_type,
|
|
proof_account,
|
|
proof_offset,
|
|
context_state,
|
|
} => {
|
|
match inspect_zk_verify(
|
|
pool,
|
|
request,
|
|
&mut report,
|
|
*proof_type,
|
|
std::option::Option::Some((proof_account, *proof_offset)),
|
|
context_state.as_ref(),
|
|
)
|
|
.await
|
|
{
|
|
std::result::Result::Ok(()) => {},
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(error);
|
|
},
|
|
}
|
|
},
|
|
kb_lib::ExSolanaCoreOperation::ZkElGamalCloseContextState {
|
|
context_state,
|
|
authority,
|
|
..
|
|
} => match inspect_zk_close(pool, request, &mut report, context_state, authority).await {
|
|
std::result::Result::Ok(()) => {},
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(error);
|
|
},
|
|
},
|
|
_ => {
|
|
report.status = crate::SolanaCoreStatefulReadinessStatus::NotRequired;
|
|
push_fact(&mut report, "stateful_policy", "not_required_for_operation");
|
|
return std::result::Result::Ok(report);
|
|
},
|
|
}
|
|
finalize_report(&mut report);
|
|
return std::result::Result::Ok(report);
|
|
}
|
|
|
|
fn new_report(
|
|
request: &crate::SolanaCoreStatefulReadinessRequest,
|
|
) -> crate::SolanaCoreStatefulReadinessReport {
|
|
return crate::SolanaCoreStatefulReadinessReport {
|
|
cluster: request.cluster,
|
|
operation_code: request.operation.operation_code().to_string(),
|
|
status: crate::SolanaCoreStatefulReadinessStatus::Ready,
|
|
context_slot: std::option::Option::None,
|
|
checks: std::vec::Vec::new(),
|
|
facts: std::vec::Vec::new(),
|
|
};
|
|
}
|
|
|
|
fn validate_cluster(
|
|
expected: kb_lib::ExApiExecutionCluster,
|
|
genesis: &kb_onchain_transport::GenesisHashResult,
|
|
) -> kb_core::Result<()> {
|
|
return match expected {
|
|
kb_lib::ExApiExecutionCluster::Devnet => {
|
|
if genesis.classified_cluster
|
|
== std::option::Option::Some(kb_lib::ExApiExecutionCluster::Devnet)
|
|
{
|
|
std::result::Result::Ok(())
|
|
} else {
|
|
std::result::Result::Err(kb_core::Error::new(
|
|
"execution_stateful_devnet_genesis_mismatch",
|
|
"stateful Devnet readiness requires the official Devnet genesis hash",
|
|
))
|
|
}
|
|
},
|
|
kb_lib::ExApiExecutionCluster::Localnet => {
|
|
if genesis.classified_cluster.is_none() {
|
|
std::result::Result::Ok(())
|
|
} else {
|
|
std::result::Result::Err(kb_core::Error::new(
|
|
"execution_stateful_localnet_public_cluster",
|
|
"Localnet readiness refuses endpoints classified as an official public cluster",
|
|
))
|
|
}
|
|
},
|
|
kb_lib::ExApiExecutionCluster::Testnet | kb_lib::ExApiExecutionCluster::Mainnet => {
|
|
std::result::Result::Err(kb_core::Error::new(
|
|
"execution_stateful_cluster_unsupported",
|
|
"stateful native execution readiness is restricted to Localnet and Devnet",
|
|
))
|
|
},
|
|
};
|
|
}
|
|
|
|
fn push_check(
|
|
report: &mut crate::SolanaCoreStatefulReadinessReport,
|
|
code: &str,
|
|
passed: bool,
|
|
message: impl std::convert::Into<std::string::String>,
|
|
) {
|
|
report.checks.push(crate::SolanaCoreStatefulCheck {
|
|
code: code.to_string(),
|
|
passed,
|
|
message: message.into(),
|
|
});
|
|
}
|
|
|
|
fn push_fact(
|
|
report: &mut crate::SolanaCoreStatefulReadinessReport,
|
|
key: &str,
|
|
value: impl std::fmt::Display,
|
|
) {
|
|
report.facts.push(crate::SolanaCoreStatefulFact {
|
|
key: key.to_string(),
|
|
value: value.to_string(),
|
|
});
|
|
}
|
|
|
|
fn update_context_slot(report: &mut crate::SolanaCoreStatefulReadinessReport, slot: u64) {
|
|
let current = match report.context_slot {
|
|
std::option::Option::Some(value) => value.max(slot),
|
|
std::option::Option::None => slot,
|
|
};
|
|
report.context_slot = std::option::Option::Some(current);
|
|
}
|
|
|
|
fn finalize_report(report: &mut crate::SolanaCoreStatefulReadinessReport) {
|
|
report.status = if report.checks.iter().all(|check| return check.passed) {
|
|
crate::SolanaCoreStatefulReadinessStatus::Ready
|
|
} else {
|
|
crate::SolanaCoreStatefulReadinessStatus::Blocked
|
|
};
|
|
}
|
|
|
|
async fn account_with_data(
|
|
pool: &kb_onchain_transport::HttpEndpointPool,
|
|
request: &crate::SolanaCoreStatefulReadinessRequest,
|
|
pubkey: &kb_lib::MdPubkey,
|
|
) -> kb_core::Result<kb_onchain_transport::AccountInfoResult> {
|
|
let config = match kb_onchain_transport::GetAccountInfoConfig::confirmed_with_data(
|
|
COMPLETE_ACCOUNT_DATA_LIMIT,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
return pool
|
|
.get_account_info_for_role(request.query_role.as_str(), pubkey, &config)
|
|
.await;
|
|
}
|
|
|
|
async fn rent_minimum(
|
|
pool: &kb_onchain_transport::HttpEndpointPool,
|
|
request: &crate::SolanaCoreStatefulReadinessRequest,
|
|
data_length: u64,
|
|
) -> kb_core::Result<u64> {
|
|
let result = pool
|
|
.get_minimum_balance_for_rent_exemption_for_role(
|
|
request.query_role.as_str(),
|
|
data_length,
|
|
&kb_onchain_transport::GetMinimumBalanceForRentExemptionConfig::confirmed(),
|
|
)
|
|
.await;
|
|
return match result {
|
|
std::result::Result::Ok(value) => std::result::Result::Ok(value.minimum_balance_lamports),
|
|
std::result::Result::Err(error) => std::result::Result::Err(error),
|
|
};
|
|
}
|
|
|
|
async fn epoch_info(
|
|
pool: &kb_onchain_transport::HttpEndpointPool,
|
|
request: &crate::SolanaCoreStatefulReadinessRequest,
|
|
) -> kb_core::Result<kb_onchain_transport::EpochInfoResult> {
|
|
return pool
|
|
.get_epoch_info_for_role(
|
|
request.query_role.as_str(),
|
|
&kb_onchain_transport::GetEpochInfoConfig::confirmed(),
|
|
)
|
|
.await;
|
|
}
|
|
|
|
async fn inspect_alt_create(
|
|
pool: &kb_onchain_transport::HttpEndpointPool,
|
|
request: &crate::SolanaCoreStatefulReadinessRequest,
|
|
report: &mut crate::SolanaCoreStatefulReadinessReport,
|
|
authority: &kb_lib::MdPubkey,
|
|
payer: &kb_lib::MdPubkey,
|
|
recent_slot: u64,
|
|
) -> kb_core::Result<()> {
|
|
let authority_address = match solana_pubkey::Pubkey::from_str(authority.0.as_str()) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(kb_core::Error::config(format!(
|
|
"invalid ALT authority: {error}"
|
|
)));
|
|
},
|
|
};
|
|
let payer_address = match solana_pubkey::Pubkey::from_str(payer.0.as_str()) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(kb_core::Error::config(format!(
|
|
"invalid ALT payer: {error}"
|
|
)));
|
|
},
|
|
};
|
|
let (_, table_address) =
|
|
solana_address_lookup_table_interface::instruction::create_lookup_table(
|
|
authority_address,
|
|
payer_address,
|
|
recent_slot,
|
|
);
|
|
let table = kb_lib::MdPubkey(table_address.to_string());
|
|
let account_result = match account_with_data(pool, request, &table).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
update_context_slot(report, account_result.context.slot);
|
|
push_fact(report, "lookup_table", table.0.as_str());
|
|
push_check(
|
|
report,
|
|
"alt_create_target_absent",
|
|
account_result.account.is_none(),
|
|
"derived lookup table account must not already exist",
|
|
);
|
|
let epoch = match epoch_info(pool, request).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
push_fact(report, "absolute_slot", epoch.absolute_slot);
|
|
let last_valid =
|
|
solana_address_lookup_table_interface::state::estimate_last_valid_slot(recent_slot);
|
|
push_fact(report, "recent_slot_last_valid", last_valid);
|
|
push_check(
|
|
report,
|
|
"alt_create_recent_slot_not_future",
|
|
recent_slot <= epoch.absolute_slot,
|
|
"ALT recent slot must not be ahead of the current confirmed slot",
|
|
);
|
|
push_check(
|
|
report,
|
|
"alt_create_recent_slot_still_recent",
|
|
epoch.absolute_slot <= last_valid,
|
|
"ALT recent slot must remain in the runtime slot-hashes window",
|
|
);
|
|
let rent = match rent_minimum(pool, request, ALT_META_SIZE as u64).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
push_fact(report, "minimum_rent_lamports", rent);
|
|
return std::result::Result::Ok(());
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
enum AltAction {
|
|
Extend {
|
|
payer_present: bool,
|
|
added_addresses: usize,
|
|
},
|
|
Freeze,
|
|
Deactivate,
|
|
Close,
|
|
}
|
|
|
|
async fn inspect_alt_existing(
|
|
pool: &kb_onchain_transport::HttpEndpointPool,
|
|
request: &crate::SolanaCoreStatefulReadinessRequest,
|
|
report: &mut crate::SolanaCoreStatefulReadinessReport,
|
|
lookup_table: &kb_lib::MdPubkey,
|
|
authority: &kb_lib::MdPubkey,
|
|
action: AltAction,
|
|
) -> kb_core::Result<()> {
|
|
let account_result = match account_with_data(pool, request, lookup_table).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
update_context_slot(report, account_result.context.slot);
|
|
let account = match account_result.account {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => {
|
|
push_check(report, "alt_account_exists", false, "lookup table account does not exist");
|
|
return std::result::Result::Ok(());
|
|
},
|
|
};
|
|
push_check(
|
|
report,
|
|
"alt_owner_matches",
|
|
account.owner.0 == kb_program_ids::ADDRESS_LOOKUP_TABLE_PROGRAM_ID,
|
|
"lookup table account must be owned by the Address Lookup Table program",
|
|
);
|
|
push_check(
|
|
report,
|
|
"alt_not_executable",
|
|
!account.executable,
|
|
"lookup table state account must not be executable",
|
|
);
|
|
let state = match parse_alt_state(account.data.as_slice()) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
push_check(report, "alt_state_decodes", false, error.to_string());
|
|
return std::result::Result::Ok(());
|
|
},
|
|
};
|
|
push_fact(report, "alt_address_count", state.address_count);
|
|
push_fact(report, "alt_deactivation_slot", state.deactivation_slot);
|
|
push_check(
|
|
report,
|
|
"alt_authority_matches",
|
|
state.authority.as_deref() == std::option::Option::Some(authority.0.as_str()),
|
|
"lookup table authority must match the requested authority",
|
|
);
|
|
let active = state.deactivation_slot == u64::MAX;
|
|
match action {
|
|
AltAction::Extend { payer_present, added_addresses } => {
|
|
push_check(
|
|
report,
|
|
"alt_extend_active",
|
|
active,
|
|
"lookup table must be active before extension",
|
|
);
|
|
let target_count = state.address_count.saturating_add(added_addresses);
|
|
push_fact(report, "alt_target_address_count", target_count);
|
|
push_check(
|
|
report,
|
|
"alt_extend_capacity",
|
|
target_count <= ALT_MAX_ADDRESSES,
|
|
"lookup table cannot exceed 256 addresses",
|
|
);
|
|
let target_space = ALT_META_SIZE.saturating_add(target_count.saturating_mul(32));
|
|
let rent = match rent_minimum(pool, request, target_space as u64).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let top_up = rent.saturating_sub(account.lamports);
|
|
push_fact(report, "alt_target_space", target_space);
|
|
push_fact(report, "alt_rent_top_up_lamports", top_up);
|
|
push_check(
|
|
report,
|
|
"alt_extend_payer_available_when_needed",
|
|
top_up == 0 || payer_present,
|
|
"ALT extension requires the optional payer/system pair when rent top-up is needed",
|
|
);
|
|
},
|
|
AltAction::Freeze => {
|
|
push_check(
|
|
report,
|
|
"alt_freeze_active",
|
|
active,
|
|
"lookup table must be active before freezing",
|
|
);
|
|
push_check(
|
|
report,
|
|
"alt_freeze_non_empty",
|
|
state.address_count > 0,
|
|
"empty lookup tables cannot be frozen",
|
|
);
|
|
},
|
|
AltAction::Deactivate => {
|
|
push_check(
|
|
report,
|
|
"alt_deactivate_active",
|
|
active,
|
|
"lookup table must still be active before deactivation",
|
|
);
|
|
},
|
|
AltAction::Close => {
|
|
push_check(
|
|
report,
|
|
"alt_close_deactivated",
|
|
!active,
|
|
"lookup table must be deactivated before closing",
|
|
);
|
|
if !active {
|
|
let epoch = match epoch_info(pool, request).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let last_valid =
|
|
solana_address_lookup_table_interface::state::estimate_last_valid_slot(
|
|
state.deactivation_slot,
|
|
);
|
|
push_fact(report, "alt_close_last_valid_slot", last_valid);
|
|
push_check(
|
|
report,
|
|
"alt_close_cooldown_elapsed",
|
|
epoch.absolute_slot > last_valid,
|
|
"lookup table cooldown must be conservatively elapsed before close",
|
|
);
|
|
}
|
|
},
|
|
}
|
|
return std::result::Result::Ok(());
|
|
}
|
|
|
|
fn parse_alt_state(data: &[u8]) -> kb_core::Result<AddressLookupTableState> {
|
|
if data.len() < ALT_META_SIZE {
|
|
return std::result::Result::Err(kb_core::Error::new(
|
|
"execution_stateful_alt_data_short",
|
|
"lookup table account data is shorter than the 56-byte metadata region",
|
|
));
|
|
}
|
|
let tag = match read_u32(data, 0, "ALT program state tag") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
if tag != 1 {
|
|
return std::result::Result::Err(kb_core::Error::new(
|
|
"execution_stateful_alt_uninitialized",
|
|
"lookup table account is not in the initialized LookupTable program state",
|
|
));
|
|
}
|
|
let deactivation_slot = match read_u64(data, 4, "ALT deactivation slot") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let authority = match data[21] {
|
|
0 => std::option::Option::None,
|
|
1 => {
|
|
let bytes = match data.get(22..54) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => {
|
|
return std::result::Result::Err(kb_core::Error::new(
|
|
"execution_stateful_alt_authority_short",
|
|
"lookup table authority bytes are truncated",
|
|
));
|
|
},
|
|
};
|
|
let array = match <[u8; 32]>::try_from(bytes) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(kb_core::Error::new(
|
|
"execution_stateful_alt_authority_short",
|
|
"lookup table authority bytes are not exactly 32 bytes",
|
|
));
|
|
},
|
|
};
|
|
std::option::Option::Some(solana_pubkey::Pubkey::new_from_array(array).to_string())
|
|
},
|
|
value => {
|
|
return std::result::Result::Err(kb_core::Error::new(
|
|
"execution_stateful_alt_authority_option_invalid",
|
|
format!("lookup table authority option tag must be 0 or 1 but received {value}"),
|
|
));
|
|
},
|
|
};
|
|
let address_bytes = data.len() - ALT_META_SIZE;
|
|
if address_bytes % 32 != 0 {
|
|
return std::result::Result::Err(kb_core::Error::new(
|
|
"execution_stateful_alt_addresses_misaligned",
|
|
"lookup table address bytes must be aligned to 32-byte public keys",
|
|
));
|
|
}
|
|
let address_count = address_bytes / 32;
|
|
if address_count > ALT_MAX_ADDRESSES {
|
|
return std::result::Result::Err(kb_core::Error::new(
|
|
"execution_stateful_alt_capacity_invalid",
|
|
"lookup table account contains more than 256 addresses",
|
|
));
|
|
}
|
|
return std::result::Result::Ok(AddressLookupTableState {
|
|
deactivation_slot,
|
|
authority,
|
|
address_count,
|
|
});
|
|
}
|
|
|
|
async fn inspect_config_create(
|
|
pool: &kb_onchain_transport::HttpEndpointPool,
|
|
request: &crate::SolanaCoreStatefulReadinessRequest,
|
|
report: &mut crate::SolanaCoreStatefulReadinessReport,
|
|
config_account: &kb_lib::MdPubkey,
|
|
lamports: u64,
|
|
max_data_space: u64,
|
|
max_key_count: usize,
|
|
) -> kb_core::Result<()> {
|
|
let account_result = match account_with_data(pool, request, config_account).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
update_context_slot(report, account_result.context.slot);
|
|
push_check(
|
|
report,
|
|
"config_create_target_absent",
|
|
account_result.account.is_none(),
|
|
"new Config account must not already exist",
|
|
);
|
|
let key_bytes =
|
|
compact_u16_length(max_key_count).saturating_add(max_key_count.saturating_mul(33));
|
|
let space = match max_data_space.checked_add(key_bytes as u64) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => {
|
|
return std::result::Result::Err(kb_core::Error::new(
|
|
"execution_stateful_config_space_overflow",
|
|
"Config account allocation length overflow",
|
|
));
|
|
},
|
|
};
|
|
let rent = match rent_minimum(pool, request, space).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
push_fact(report, "config_account_space", space);
|
|
push_fact(report, "minimum_rent_lamports", rent);
|
|
push_check(
|
|
report,
|
|
"config_create_rent_sufficient",
|
|
lamports >= rent,
|
|
"Config account lamports must meet the current rent-exempt minimum",
|
|
);
|
|
return std::result::Result::Ok(());
|
|
}
|
|
|
|
async fn inspect_config_store(
|
|
pool: &kb_onchain_transport::HttpEndpointPool,
|
|
request: &crate::SolanaCoreStatefulReadinessRequest,
|
|
report: &mut crate::SolanaCoreStatefulReadinessReport,
|
|
config_account: &kb_lib::MdPubkey,
|
|
key_count: usize,
|
|
data_length: usize,
|
|
) -> kb_core::Result<()> {
|
|
let account_result = match account_with_data(pool, request, config_account).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
update_context_slot(report, account_result.context.slot);
|
|
let account = match account_result.account {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => {
|
|
push_check(
|
|
report,
|
|
"config_store_account_exists",
|
|
false,
|
|
"Config account does not exist",
|
|
);
|
|
return std::result::Result::Ok(());
|
|
},
|
|
};
|
|
push_check(
|
|
report,
|
|
"config_store_owner_matches",
|
|
account.owner.0 == kb_program_ids::CONFIG_PROGRAM_ID,
|
|
"Config account must be owned by the Config program",
|
|
);
|
|
push_check(
|
|
report,
|
|
"config_store_not_executable",
|
|
!account.executable,
|
|
"Config state account must not be executable",
|
|
);
|
|
let required = compact_u16_length(key_count)
|
|
.saturating_add(key_count.saturating_mul(33))
|
|
.saturating_add(data_length);
|
|
push_fact(report, "config_required_space", required);
|
|
push_fact(report, "config_existing_space", account.space);
|
|
push_check(
|
|
report,
|
|
"config_store_space_sufficient",
|
|
account.space >= required as u64,
|
|
"Config account allocation must fit the complete encoded key list and opaque data",
|
|
);
|
|
return std::result::Result::Ok(());
|
|
}
|
|
|
|
async fn inspect_feature_activate(
|
|
pool: &kb_onchain_transport::HttpEndpointPool,
|
|
request: &crate::SolanaCoreStatefulReadinessRequest,
|
|
report: &mut crate::SolanaCoreStatefulReadinessReport,
|
|
feature_account: &kb_lib::MdPubkey,
|
|
lamports: u64,
|
|
) -> kb_core::Result<()> {
|
|
let account_result = match account_with_data(pool, request, feature_account).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
update_context_slot(report, account_result.context.slot);
|
|
let existing_lamports = match account_result.account.as_ref() {
|
|
std::option::Option::Some(account) => account.lamports,
|
|
std::option::Option::None => 0,
|
|
};
|
|
let reusable = account_result.account.as_ref().map(|account| {
|
|
return account.owner.0 == kb_program_ids::SYSTEM_PROGRAM_ID
|
|
&& !account.executable
|
|
&& account.space == 0
|
|
&& account.data.is_empty();
|
|
});
|
|
push_check(
|
|
report,
|
|
"feature_activate_target_available",
|
|
match reusable {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => true,
|
|
},
|
|
"feature target must be absent or a prefunded zero-space System account",
|
|
);
|
|
let rent = match rent_minimum(pool, request, FEATURE_ACCOUNT_SIZE).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let required_top_up = rent.saturating_sub(existing_lamports);
|
|
push_fact(report, "feature_existing_lamports", existing_lamports);
|
|
push_fact(report, "feature_required_top_up_lamports", required_top_up);
|
|
push_check(
|
|
report,
|
|
"feature_activate_rent_sufficient",
|
|
lamports >= required_top_up,
|
|
"feature activation transfer must cover the measured rent shortfall",
|
|
);
|
|
return std::result::Result::Ok(());
|
|
}
|
|
|
|
async fn inspect_feature_revoke(
|
|
pool: &kb_onchain_transport::HttpEndpointPool,
|
|
request: &crate::SolanaCoreStatefulReadinessRequest,
|
|
report: &mut crate::SolanaCoreStatefulReadinessReport,
|
|
feature_account: &kb_lib::MdPubkey,
|
|
) -> kb_core::Result<()> {
|
|
let account_result = match account_with_data(pool, request, feature_account).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
update_context_slot(report, account_result.context.slot);
|
|
let account = match account_result.account {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => {
|
|
push_check(
|
|
report,
|
|
"feature_revoke_account_exists",
|
|
false,
|
|
"feature account does not exist",
|
|
);
|
|
return std::result::Result::Ok(());
|
|
},
|
|
};
|
|
push_check(
|
|
report,
|
|
"feature_revoke_owner_matches",
|
|
account.owner.0 == kb_program_ids::FEATURE_PROGRAM_ID,
|
|
"feature account must be owned by the Feature program",
|
|
);
|
|
push_check(
|
|
report,
|
|
"feature_revoke_not_executable",
|
|
!account.executable,
|
|
"feature state account must not be executable",
|
|
);
|
|
let pending = account.data.len() == FEATURE_ACCOUNT_SIZE as usize
|
|
&& account.data.first() == std::option::Option::Some(&0);
|
|
push_check(
|
|
report,
|
|
"feature_revoke_pending",
|
|
pending,
|
|
"only a pending feature state encoded as Feature { activated_at: None } may be revoked",
|
|
);
|
|
return std::result::Result::Ok(());
|
|
}
|
|
|
|
async fn inspect_slashing_submit(
|
|
pool: &kb_onchain_transport::HttpEndpointPool,
|
|
request: &crate::SolanaCoreStatefulReadinessRequest,
|
|
report: &mut crate::SolanaCoreStatefulReadinessReport,
|
|
proof_account: &kb_lib::MdPubkey,
|
|
report_account: &kb_lib::MdPubkey,
|
|
report_lamports: u64,
|
|
proof_offset: u64,
|
|
slot: u64,
|
|
) -> kb_core::Result<()> {
|
|
let proof_result = match account_with_data(pool, request, proof_account).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
update_context_slot(report, proof_result.context.slot);
|
|
let proof = match proof_result.account {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => {
|
|
push_check(
|
|
report,
|
|
"slashing_proof_account_exists",
|
|
false,
|
|
"duplicate-block proof account does not exist",
|
|
);
|
|
return std::result::Result::Ok(());
|
|
},
|
|
};
|
|
push_check(
|
|
report,
|
|
"slashing_proof_not_executable",
|
|
!proof.executable,
|
|
"duplicate-block proof account must not be executable",
|
|
);
|
|
let packed_length = match duplicate_block_proof_length(proof.data.as_slice(), proof_offset) {
|
|
std::result::Result::Ok(value) => {
|
|
push_check(
|
|
report,
|
|
"slashing_proof_shape_valid",
|
|
true,
|
|
"duplicate-block proof contains two bounded length-prefixed shreds",
|
|
);
|
|
value
|
|
},
|
|
std::result::Result::Err(error) => {
|
|
push_check(report, "slashing_proof_shape_valid", false, error.to_string());
|
|
0
|
|
},
|
|
};
|
|
push_fact(report, "slashing_packed_proof_bytes", packed_length);
|
|
let report_result = match account_with_data(pool, request, report_account).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
update_context_slot(report, report_result.context.slot);
|
|
let report_reusable = report_result.account.as_ref().map(|account| {
|
|
return account.owner.0 == kb_program_ids::SYSTEM_PROGRAM_ID
|
|
&& !account.executable
|
|
&& account.space == 0
|
|
&& account.data.is_empty();
|
|
});
|
|
push_check(
|
|
report,
|
|
"slashing_report_target_available",
|
|
match report_reusable {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => true,
|
|
},
|
|
"violation report PDA must be absent or an empty prefunded System account",
|
|
);
|
|
let epoch = match epoch_info(pool, request).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
push_fact(report, "absolute_slot", epoch.absolute_slot);
|
|
push_fact(report, "slots_in_epoch", epoch.slots_in_epoch);
|
|
let age = epoch.absolute_slot.saturating_sub(slot);
|
|
push_check(
|
|
report,
|
|
"slashing_slot_not_future",
|
|
slot <= epoch.absolute_slot,
|
|
"reported duplicate-block slot must not be in the future",
|
|
);
|
|
push_check(
|
|
report,
|
|
"slashing_slot_within_one_epoch",
|
|
slot <= epoch.absolute_slot && age <= epoch.slots_in_epoch,
|
|
"duplicate-block proof must be submitted within one epoch worth of slots",
|
|
);
|
|
if packed_length > 0 {
|
|
let report_size = SLASHING_REPORT_HEADER_SIZE.saturating_add(packed_length);
|
|
let rent = match rent_minimum(pool, request, report_size as u64).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
push_fact(report, "slashing_report_space", report_size);
|
|
push_fact(report, "minimum_rent_lamports", rent);
|
|
push_check(
|
|
report,
|
|
"slashing_report_rent_sufficient",
|
|
report_lamports >= rent,
|
|
"report_lamports must cover the exact report header and retained proof bytes",
|
|
);
|
|
}
|
|
return std::result::Result::Ok(());
|
|
}
|
|
|
|
async fn inspect_slashing_close(
|
|
pool: &kb_onchain_transport::HttpEndpointPool,
|
|
request: &crate::SolanaCoreStatefulReadinessRequest,
|
|
report: &mut crate::SolanaCoreStatefulReadinessReport,
|
|
report_account: &kb_lib::MdPubkey,
|
|
destination: &kb_lib::MdPubkey,
|
|
) -> kb_core::Result<()> {
|
|
let account_result = match account_with_data(pool, request, report_account).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
update_context_slot(report, account_result.context.slot);
|
|
let account = match account_result.account {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => {
|
|
push_check(
|
|
report,
|
|
"slashing_report_exists",
|
|
false,
|
|
"violation report account does not exist",
|
|
);
|
|
return std::result::Result::Ok(());
|
|
},
|
|
};
|
|
push_check(
|
|
report,
|
|
"slashing_report_owner_matches",
|
|
account.owner.0 == kb_program_ids::SLASHING_PROGRAM_ID,
|
|
"violation report must be owned by the Slashing program",
|
|
);
|
|
push_check(
|
|
report,
|
|
"slashing_report_not_executable",
|
|
!account.executable,
|
|
"violation report state account must not be executable",
|
|
);
|
|
if account.data.len() < SLASHING_REPORT_HEADER_SIZE {
|
|
push_check(
|
|
report,
|
|
"slashing_report_decodes",
|
|
false,
|
|
"violation report is shorter than its 114-byte fixed header",
|
|
);
|
|
return std::result::Result::Ok(());
|
|
}
|
|
let version = account.data[0];
|
|
let retained_destination = match pubkey_text(&account.data[33..65]) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
push_check(report, "slashing_report_decodes", false, error.to_string());
|
|
return std::result::Result::Ok(());
|
|
},
|
|
};
|
|
let report_epoch = match read_u64(account.data.as_slice(), 65, "Slashing report epoch") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
push_fact(report, "slashing_report_version", version);
|
|
push_fact(report, "slashing_report_epoch", report_epoch);
|
|
push_check(
|
|
report,
|
|
"slashing_report_version_supported",
|
|
version == 1,
|
|
"violation report version must be 1",
|
|
);
|
|
push_check(
|
|
report,
|
|
"slashing_report_destination_matches",
|
|
retained_destination == destination.0,
|
|
"close destination must match the destination retained in the violation report",
|
|
);
|
|
let epoch = match epoch_info(pool, request).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
push_fact(report, "current_epoch", epoch.epoch);
|
|
push_check(
|
|
report,
|
|
"slashing_report_retention_elapsed",
|
|
epoch.epoch >= report_epoch.saturating_add(3),
|
|
"violation report must remain on-chain for at least three epochs",
|
|
);
|
|
return std::result::Result::Ok(());
|
|
}
|
|
|
|
async fn inspect_zk_verify(
|
|
pool: &kb_onchain_transport::HttpEndpointPool,
|
|
request: &crate::SolanaCoreStatefulReadinessRequest,
|
|
report: &mut crate::SolanaCoreStatefulReadinessReport,
|
|
proof_type: kb_lib::ExSolanaCoreZkElGamalProofType,
|
|
proof_account: std::option::Option<(&kb_lib::MdPubkey, u32)>,
|
|
context_state: std::option::Option<&kb_lib::ExSolanaCoreZkElGamalContextState>,
|
|
) -> kb_core::Result<()> {
|
|
if let std::option::Option::Some((account_pubkey, offset)) = proof_account {
|
|
let proof_result = match account_with_data(pool, request, account_pubkey).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
update_context_slot(report, proof_result.context.slot);
|
|
let proof = match proof_result.account {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => {
|
|
push_check(
|
|
report,
|
|
"zk_proof_account_exists",
|
|
false,
|
|
"ZK proof account does not exist",
|
|
);
|
|
return std::result::Result::Ok(());
|
|
},
|
|
};
|
|
push_check(
|
|
report,
|
|
"zk_proof_account_not_executable",
|
|
!proof.executable,
|
|
"ZK proof account must not be executable",
|
|
);
|
|
let end = (offset as usize).checked_add(proof_type.proof_data_size());
|
|
push_check(
|
|
report,
|
|
"zk_proof_range_available",
|
|
match end {
|
|
std::option::Option::Some(end) => end <= proof.data.len(),
|
|
std::option::Option::None => false,
|
|
},
|
|
"ZK proof account must contain the complete proof POD at the requested offset",
|
|
);
|
|
}
|
|
let context = match context_state {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => {
|
|
push_fact(report, "zk_context_state", "not_requested");
|
|
return std::result::Result::Ok(());
|
|
},
|
|
};
|
|
let context_result = match account_with_data(pool, request, &context.account).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
update_context_slot(report, context_result.context.slot);
|
|
let account = match context_result.account {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => {
|
|
push_check(
|
|
report,
|
|
"zk_context_account_exists",
|
|
false,
|
|
"preallocated ZK context-state account does not exist",
|
|
);
|
|
return std::result::Result::Ok(());
|
|
},
|
|
};
|
|
let expected_size = proof_type.context_state_size();
|
|
push_fact(report, "zk_context_expected_space", expected_size);
|
|
push_fact(report, "zk_context_existing_space", account.space);
|
|
push_check(
|
|
report,
|
|
"zk_context_owner_matches",
|
|
account.owner.0 == kb_program_ids::ZK_ELGAMAL_PROOF_PROGRAM_ID,
|
|
"ZK context-state account must be owned by the ZK ElGamal Proof program",
|
|
);
|
|
push_check(
|
|
report,
|
|
"zk_context_not_executable",
|
|
!account.executable,
|
|
"ZK context-state account must not be executable",
|
|
);
|
|
push_check(
|
|
report,
|
|
"zk_context_space_exact",
|
|
account.space == expected_size as u64 && account.data.len() == expected_size,
|
|
"ZK context-state allocation must exactly match the official ProofContextState size",
|
|
);
|
|
push_check(
|
|
report,
|
|
"zk_context_uninitialized",
|
|
account.data.iter().all(|byte| return *byte == 0),
|
|
"preallocated ZK context-state account must still be zero-initialized",
|
|
);
|
|
let rent = match rent_minimum(pool, request, expected_size as u64).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
push_fact(report, "minimum_rent_lamports", rent);
|
|
push_check(
|
|
report,
|
|
"zk_context_rent_exempt",
|
|
account.lamports >= rent,
|
|
"preallocated ZK context-state account must be rent exempt",
|
|
);
|
|
return std::result::Result::Ok(());
|
|
}
|
|
|
|
async fn inspect_zk_close(
|
|
pool: &kb_onchain_transport::HttpEndpointPool,
|
|
request: &crate::SolanaCoreStatefulReadinessRequest,
|
|
report: &mut crate::SolanaCoreStatefulReadinessReport,
|
|
context_state: &kb_lib::MdPubkey,
|
|
authority: &kb_lib::MdPubkey,
|
|
) -> kb_core::Result<()> {
|
|
let account_result = match account_with_data(pool, request, context_state).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
update_context_slot(report, account_result.context.slot);
|
|
let account = match account_result.account {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => {
|
|
push_check(
|
|
report,
|
|
"zk_context_account_exists",
|
|
false,
|
|
"ZK context-state account does not exist",
|
|
);
|
|
return std::result::Result::Ok(());
|
|
},
|
|
};
|
|
push_check(
|
|
report,
|
|
"zk_context_owner_matches",
|
|
account.owner.0 == kb_program_ids::ZK_ELGAMAL_PROOF_PROGRAM_ID,
|
|
"ZK context-state account must be owned by the ZK ElGamal Proof program",
|
|
);
|
|
push_check(
|
|
report,
|
|
"zk_context_not_executable",
|
|
!account.executable,
|
|
"ZK context-state account must not be executable",
|
|
);
|
|
if account.data.len() < ZK_CONTEXT_META_SIZE {
|
|
push_check(
|
|
report,
|
|
"zk_context_meta_decodes",
|
|
false,
|
|
"ZK context-state account is shorter than its 33-byte generic metadata",
|
|
);
|
|
return std::result::Result::Ok(());
|
|
}
|
|
let retained_authority = match pubkey_text(&account.data[..32]) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
push_check(report, "zk_context_meta_decodes", false, error.to_string());
|
|
return std::result::Result::Ok(());
|
|
},
|
|
};
|
|
let proof_type = proof_type_from_discriminator(account.data[32]);
|
|
push_check(
|
|
report,
|
|
"zk_context_authority_matches",
|
|
retained_authority == authority.0,
|
|
"close authority must match the authority retained in ProofContextState",
|
|
);
|
|
push_check(
|
|
report,
|
|
"zk_context_proof_type_supported",
|
|
proof_type.is_some(),
|
|
"ProofContextState must contain a supported proof type discriminator",
|
|
);
|
|
if let std::option::Option::Some(value) = proof_type {
|
|
let expected_size = value.context_state_size();
|
|
push_fact(report, "zk_context_proof_type", value.discriminator());
|
|
push_fact(report, "zk_context_expected_space", expected_size);
|
|
push_check(
|
|
report,
|
|
"zk_context_space_exact",
|
|
account.space == expected_size as u64 && account.data.len() == expected_size,
|
|
"stored ZK context-state allocation must match its proof type",
|
|
);
|
|
}
|
|
return std::result::Result::Ok(());
|
|
}
|
|
|
|
fn proof_type_from_discriminator(
|
|
discriminator: u8,
|
|
) -> std::option::Option<kb_lib::ExSolanaCoreZkElGamalProofType> {
|
|
return match discriminator {
|
|
1 => std::option::Option::Some(kb_lib::ExSolanaCoreZkElGamalProofType::ZeroCiphertext),
|
|
2 => std::option::Option::Some(
|
|
kb_lib::ExSolanaCoreZkElGamalProofType::CiphertextCiphertextEquality,
|
|
),
|
|
3 => std::option::Option::Some(
|
|
kb_lib::ExSolanaCoreZkElGamalProofType::CiphertextCommitmentEquality,
|
|
),
|
|
4 => std::option::Option::Some(kb_lib::ExSolanaCoreZkElGamalProofType::PubkeyValidity),
|
|
5 => std::option::Option::Some(kb_lib::ExSolanaCoreZkElGamalProofType::PercentageWithCap),
|
|
6 => {
|
|
std::option::Option::Some(kb_lib::ExSolanaCoreZkElGamalProofType::BatchedRangeProofU64)
|
|
},
|
|
7 => {
|
|
std::option::Option::Some(kb_lib::ExSolanaCoreZkElGamalProofType::BatchedRangeProofU128)
|
|
},
|
|
8 => {
|
|
std::option::Option::Some(kb_lib::ExSolanaCoreZkElGamalProofType::BatchedRangeProofU256)
|
|
},
|
|
9 => std::option::Option::Some(
|
|
kb_lib::ExSolanaCoreZkElGamalProofType::GroupedCiphertext2HandlesValidity,
|
|
),
|
|
10 => std::option::Option::Some(
|
|
kb_lib::ExSolanaCoreZkElGamalProofType::BatchedGroupedCiphertext2HandlesValidity,
|
|
),
|
|
11 => std::option::Option::Some(
|
|
kb_lib::ExSolanaCoreZkElGamalProofType::GroupedCiphertext3HandlesValidity,
|
|
),
|
|
12 => std::option::Option::Some(
|
|
kb_lib::ExSolanaCoreZkElGamalProofType::BatchedGroupedCiphertext3HandlesValidity,
|
|
),
|
|
_ => std::option::Option::None,
|
|
};
|
|
}
|
|
|
|
fn duplicate_block_proof_length(data: &[u8], offset: u64) -> kb_core::Result<usize> {
|
|
let start = match usize::try_from(offset) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(kb_core::Error::new(
|
|
"execution_stateful_slashing_offset_invalid",
|
|
"duplicate-block proof offset does not fit the local address space",
|
|
));
|
|
},
|
|
};
|
|
let first_length = match read_u32(data, start, "first duplicate-block shred length") {
|
|
std::result::Result::Ok(value) => value as usize,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
if first_length == 0 || first_length > MAX_SHRED_SIZE {
|
|
return std::result::Result::Err(kb_core::Error::new(
|
|
"execution_stateful_slashing_first_shred_length_invalid",
|
|
"first duplicate-block shred length must be between 1 and 1232 bytes",
|
|
));
|
|
}
|
|
let second_length_offset =
|
|
match start.checked_add(4).and_then(|value| return value.checked_add(first_length)) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => {
|
|
return std::result::Result::Err(kb_core::Error::new(
|
|
"execution_stateful_slashing_proof_overflow",
|
|
"duplicate-block proof length arithmetic overflow",
|
|
));
|
|
},
|
|
};
|
|
let second_length =
|
|
match read_u32(data, second_length_offset, "second duplicate-block shred length") {
|
|
std::result::Result::Ok(value) => value as usize,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
if second_length == 0 || second_length > MAX_SHRED_SIZE {
|
|
return std::result::Result::Err(kb_core::Error::new(
|
|
"execution_stateful_slashing_second_shred_length_invalid",
|
|
"second duplicate-block shred length must be between 1 and 1232 bytes",
|
|
));
|
|
}
|
|
let total = 4_usize
|
|
.checked_add(first_length)
|
|
.and_then(|value| return value.checked_add(4))
|
|
.and_then(|value| return value.checked_add(second_length));
|
|
let total = match total {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => {
|
|
return std::result::Result::Err(kb_core::Error::new(
|
|
"execution_stateful_slashing_proof_overflow",
|
|
"duplicate-block proof length arithmetic overflow",
|
|
));
|
|
},
|
|
};
|
|
let end = match start.checked_add(total) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => {
|
|
return std::result::Result::Err(kb_core::Error::new(
|
|
"execution_stateful_slashing_proof_overflow",
|
|
"duplicate-block proof end offset overflow",
|
|
));
|
|
},
|
|
};
|
|
if end > data.len() {
|
|
return std::result::Result::Err(kb_core::Error::new(
|
|
"execution_stateful_slashing_proof_truncated",
|
|
"duplicate-block proof account does not contain both declared shreds",
|
|
));
|
|
}
|
|
return std::result::Result::Ok(total);
|
|
}
|
|
|
|
fn compact_u16_length(value: usize) -> usize {
|
|
if value < 0x80 {
|
|
return 1;
|
|
}
|
|
if value < 0x4000 {
|
|
return 2;
|
|
}
|
|
return 3;
|
|
}
|
|
|
|
fn read_u32(data: &[u8], offset: usize, label: &str) -> kb_core::Result<u32> {
|
|
let end = match offset.checked_add(4) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => {
|
|
return std::result::Result::Err(kb_core::Error::new(
|
|
"execution_stateful_integer_overflow",
|
|
format!("{label} offset overflow"),
|
|
));
|
|
},
|
|
};
|
|
let bytes = match data.get(offset..end) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => {
|
|
return std::result::Result::Err(kb_core::Error::new(
|
|
"execution_stateful_integer_truncated",
|
|
format!("{label} is truncated"),
|
|
));
|
|
},
|
|
};
|
|
let array = match <[u8; 4]>::try_from(bytes) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(kb_core::Error::new(
|
|
"execution_stateful_integer_truncated",
|
|
format!("{label} is not exactly four bytes"),
|
|
));
|
|
},
|
|
};
|
|
return std::result::Result::Ok(u32::from_le_bytes(array));
|
|
}
|
|
|
|
fn read_u64(data: &[u8], offset: usize, label: &str) -> kb_core::Result<u64> {
|
|
let end = match offset.checked_add(8) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => {
|
|
return std::result::Result::Err(kb_core::Error::new(
|
|
"execution_stateful_integer_overflow",
|
|
format!("{label} offset overflow"),
|
|
));
|
|
},
|
|
};
|
|
let bytes = match data.get(offset..end) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => {
|
|
return std::result::Result::Err(kb_core::Error::new(
|
|
"execution_stateful_integer_truncated",
|
|
format!("{label} is truncated"),
|
|
));
|
|
},
|
|
};
|
|
let array = match <[u8; 8]>::try_from(bytes) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(kb_core::Error::new(
|
|
"execution_stateful_integer_truncated",
|
|
format!("{label} is not exactly eight bytes"),
|
|
));
|
|
},
|
|
};
|
|
return std::result::Result::Ok(u64::from_le_bytes(array));
|
|
}
|
|
|
|
fn pubkey_text(bytes: &[u8]) -> kb_core::Result<std::string::String> {
|
|
let array = match <[u8; 32]>::try_from(bytes) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(kb_core::Error::new(
|
|
"execution_stateful_pubkey_length_invalid",
|
|
"stateful account field is not exactly 32 bytes",
|
|
));
|
|
},
|
|
};
|
|
return std::result::Result::Ok(solana_pubkey::Pubkey::new_from_array(array).to_string());
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
#[test]
|
|
fn cluster_policy_restricts_stateful_reads_to_localnet_and_devnet() {
|
|
let devnet = kb_onchain_transport::GenesisHashResult {
|
|
genesis_hash: std::string::String::from("devnet"),
|
|
classified_cluster: std::option::Option::Some(kb_lib::ExApiExecutionCluster::Devnet),
|
|
};
|
|
assert!(super::validate_cluster(kb_lib::ExApiExecutionCluster::Devnet, &devnet,).is_ok());
|
|
assert!(
|
|
super::validate_cluster(kb_lib::ExApiExecutionCluster::Localnet, &devnet,).is_err()
|
|
);
|
|
let local = kb_onchain_transport::GenesisHashResult {
|
|
genesis_hash: std::string::String::from("local"),
|
|
classified_cluster: std::option::Option::None,
|
|
};
|
|
assert!(super::validate_cluster(kb_lib::ExApiExecutionCluster::Localnet, &local,).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn alt_state_parser_preserves_authority_lifecycle_and_capacity() {
|
|
let authority = solana_pubkey::Pubkey::new_unique();
|
|
let mut data = vec![0_u8; 56 + 64];
|
|
data[..4].copy_from_slice(&1_u32.to_le_bytes());
|
|
data[4..12].copy_from_slice(&u64::MAX.to_le_bytes());
|
|
data[21] = 1;
|
|
let authority_bytes = authority.to_bytes();
|
|
data[22..54].copy_from_slice(&authority_bytes);
|
|
let state = super::parse_alt_state(data.as_slice())
|
|
.unwrap_or_else(|error| panic!("unexpected ALT state error: {error}"));
|
|
assert_eq!(state.deactivation_slot, u64::MAX);
|
|
assert_eq!(state.authority, std::option::Option::Some(authority.to_string()));
|
|
assert_eq!(state.address_count, 2);
|
|
data.push(0);
|
|
assert!(super::parse_alt_state(data.as_slice()).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn slashing_proof_shape_is_bounded_and_exact() {
|
|
let mut data = std::vec::Vec::new();
|
|
data.extend_from_slice(&3_u32.to_le_bytes());
|
|
data.extend_from_slice(&[1, 2, 3]);
|
|
data.extend_from_slice(&2_u32.to_le_bytes());
|
|
data.extend_from_slice(&[4, 5]);
|
|
assert_eq!(
|
|
super::duplicate_block_proof_length(data.as_slice(), 0)
|
|
.unwrap_or_else(|error| panic!("unexpected proof shape error: {error}")),
|
|
13,
|
|
);
|
|
assert!(super::duplicate_block_proof_length(&data[..12], 0).is_err());
|
|
let mut too_large = std::vec::Vec::new();
|
|
too_large.extend_from_slice(&1_233_u32.to_le_bytes());
|
|
assert!(super::duplicate_block_proof_length(too_large.as_slice(), 0,).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn proof_discriminators_map_to_exact_context_sizes() {
|
|
for discriminator in 1_u8..=12 {
|
|
let proof_type = super::proof_type_from_discriminator(discriminator)
|
|
.unwrap_or_else(|| panic!("missing proof type {discriminator}"));
|
|
assert_eq!(proof_type.discriminator(), discriminator);
|
|
assert!(proof_type.context_state_size() > super::ZK_CONTEXT_META_SIZE);
|
|
}
|
|
assert_eq!(
|
|
kb_lib::ExSolanaCoreZkElGamalProofType::BatchedRangeProofU64.context_state_size(),
|
|
kb_lib::ExSolanaCoreZkElGamalProofType::BatchedRangeProofU256.context_state_size(),
|
|
);
|
|
assert!(super::proof_type_from_discriminator(0).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn compact_u16_length_matches_config_key_boundaries() {
|
|
assert_eq!(super::compact_u16_length(0), 1);
|
|
assert_eq!(super::compact_u16_length(127), 1);
|
|
assert_eq!(super::compact_u16_length(128), 2);
|
|
assert_eq!(super::compact_u16_length(16_384), 3);
|
|
}
|
|
}
|