v0.1.0-pre.033

This commit is contained in:
2026-07-25 11:52:33 +02:00
parent 5cbccba241
commit 9ad47de757
8 changed files with 1633 additions and 10 deletions

View File

@@ -112,7 +112,9 @@
- Portage des matérialisateurs natifs lifecycle, administration, compliance et staking. - Portage des matérialisateurs natifs lifecycle, administration, compliance et staking.
- Conservation de leurs 50 tests historiques et ajout du premier test aval de matérialisation. - Conservation de leurs 50 tests historiques et ajout du premier test aval de matérialisation.
## 0.1.0-pre.032 ## 0.1.0-pre.033
- Migration de linventaire Devnet Token-2022/ElGamal et des contrats de validation machine-readable dans `kb-pipeline`. - Migre les lectures stateful Token-2022 et ElGamal Registry dans `kb-pipeline`.
- Conservation des sept tests bot2 et de `docs/SPL_TOKEN_2022_VALIDATION_MATRIX.json`. - Migre le préflight Token-2022 borné et ses validations croisées.
- Conserve les 16 tests bot2 associés sans réduction fonctionnelle.
- Normalise les noms de fichiers, modules et symboles internes sur `token2022`.

View File

@@ -104,4 +104,4 @@ La tranche `0.1.0-pre.029` migre la planification et lextraction core de `kb-
La tranche `0.1.0-pre.031` ajoute lorchestration HTTP de backfill : signatures explicites ou historiques par adresse, pagination bornée, hydration via `getTransaction`, retries, annulation, reprise déterministe et persistance des observations. La tranche `0.1.0-pre.031` ajoute lorchestration HTTP de backfill : signatures explicites ou historiques par adresse, pagination bornée, hydration via `getTransaction`, retries, annulation, reprise déterministe et persistance des observations.
La migration `0.1.0-pre.032` ajoute à `kb-pipeline` linventaire stable des scénarios Devnet Token-2022/ElGamal et la validation machine-readable du jalon 0.4.6. La migration de `kb-pipeline` couvre désormais le backfill, l'extraction core, le replay de décodage, la validation Token-2022 et les lectures stateful/préflight Token-2022 et ElGamal Registry.

View File

@@ -298,4 +298,10 @@
- [ ] Valider avec Cargo sur la machine de développement. - [ ] Valider avec Cargo sur la machine de développement.
- [ ] Porter ensuite les orchestrations Solana stateful et dexécution. - [ ] Porter ensuite les orchestrations Solana stateful et dexécution.
- [x] `0.1.0-pre.032` — migrer les scénarios Devnet Token-2022/ElGamal et la matrice de validation machine-readable. ### 0.1.0-pre.033 — Pipeline stateful Token-2022
- [x] Lecture et validation stateful Token-2022.
- [x] Routage des projections matérialisées Token-2022.
- [x] Lecture et validation du registre ElGamal.
- [x] Préflight multi-comptes Token-2022 borné.
- [ ] Orchestration des preuves et de l'exécution Token-2022.

View File

@@ -1,5 +1,5 @@
# file: kb-pipeline/Cargo.toml # file: kb-pipeline/Cargo.toml
# version: 8 # version: 9
[package] [package]
name = "kb-pipeline" name = "kb-pipeline"
@@ -10,6 +10,7 @@ publish.workspace = true
[dependencies] [dependencies]
async-trait.workspace = true async-trait.workspace = true
bs58.workspace = true
chrono.workspace = true chrono.workspace = true
futures-util.workspace = true futures-util.workspace = true
kb-core = { path = "../kb-core" } kb-core = { path = "../kb-core" }
@@ -20,13 +21,12 @@ kb-store = { path = "../kb-store" }
kb-wallet = { path = "../kb-wallet" } kb-wallet = { path = "../kb-wallet" }
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
solana-pubkey.workspace = true
spl-elgamal-registry-interface.workspace = true
sha2.workspace = true sha2.workspace = true
tokio.workspace = true tokio.workspace = true
tracing.workspace = true tracing.workspace = true
uuid.workspace = true uuid.workspace = true
[dev-dependencies]
bs58.workspace = true
[lints] [lints]
workspace = true workspace = true

View File

@@ -1,5 +1,5 @@
// file: kb-pipeline/src/lib.rs // file: kb-pipeline/src/lib.rs
// version: 7 // version: 9
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
@@ -12,7 +12,10 @@ mod constants;
mod core_extraction; mod core_extraction;
mod decode_replay; mod decode_replay;
mod plan; mod plan;
mod solana_elgamal_registry_stateful;
mod solana_token2022_devnet_scenarios; mod solana_token2022_devnet_scenarios;
mod solana_token2022_preflight;
mod solana_token2022_stateful;
mod solana_token2022_validation; mod solana_token2022_validation;
/// Address category used by one targeted backfill campaign. /// Address category used by one targeted backfill campaign.
@@ -83,6 +86,20 @@ pub use self::decode_replay::new_decode_campaign_id;
pub use self::plan::PipelineStage; pub use self::plan::PipelineStage;
/// Replay selection scope. /// Replay selection scope.
pub use self::plan::ReplayScope; pub use self::plan::ReplayScope;
/// Migrated ELGAMAL_REGISTRY_STATEFUL_ACCOUNT_BYTES contract.
pub use self::solana_elgamal_registry_stateful::ELGAMAL_REGISTRY_STATEFUL_ACCOUNT_BYTES;
/// Migrated ElGamalRegistryStatefulReadRequest contract.
pub use self::solana_elgamal_registry_stateful::ElGamalRegistryStatefulReadRequest;
/// Migrated ElGamalRegistryStatefulReadResult contract.
pub use self::solana_elgamal_registry_stateful::ElGamalRegistryStatefulReadResult;
/// Migrated ElGamalRegistryStatefulSnapshot contract.
pub use self::solana_elgamal_registry_stateful::ElGamalRegistryStatefulSnapshot;
/// Migrated materialize_elgamal_registry_account_info_result contract.
pub use self::solana_elgamal_registry_stateful::materialize_elgamal_registry_account_info_result;
/// Migrated materialize_elgamal_registry_stateful_snapshot contract.
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;
/// Stable category of one independent Devnet validation scenario. /// Stable category of one independent Devnet validation scenario.
pub use self::solana_token2022_devnet_scenarios::DevnetSplValidationFamily; pub use self::solana_token2022_devnet_scenarios::DevnetSplValidationFamily;
/// Current implementation status of one Devnet validation scenario. /// Current implementation status of one Devnet validation scenario.
@@ -91,6 +108,40 @@ pub use self::solana_token2022_devnet_scenarios::DevnetSplValidationImplementati
pub use self::solana_token2022_devnet_scenarios::DevnetSplValidationScenario; pub use self::solana_token2022_devnet_scenarios::DevnetSplValidationScenario;
/// Returns the complete ordered Devnet scenario inventory. /// Returns the complete ordered Devnet scenario inventory.
pub use self::solana_token2022_devnet_scenarios::devnet_spl_validation_scenarios; pub use self::solana_token2022_devnet_scenarios::devnet_spl_validation_scenarios;
/// Migrated MAX_TOKEN2022_PREFLIGHT_ACCOUNTS contract.
pub use self::solana_token2022_preflight::MAX_TOKEN2022_PREFLIGHT_ACCOUNTS;
/// Migrated MAX_TOKEN2022_PREFLIGHT_TOTAL_BYTES contract.
pub use self::solana_token2022_preflight::MAX_TOKEN2022_PREFLIGHT_TOTAL_BYTES;
/// Migrated Token2022PreflightAccountReport contract.
pub use self::solana_token2022_preflight::Token2022PreflightAccountReport;
/// Migrated Token2022PreflightReport contract.
pub use self::solana_token2022_preflight::Token2022PreflightReport;
/// Migrated Token2022PreflightRequest contract.
pub use self::solana_token2022_preflight::Token2022PreflightRequest;
/// Migrated Token2022PreflightRequirement contract.
pub use self::solana_token2022_preflight::Token2022PreflightRequirement;
/// Migrated inspect_token2022_preflight contract.
pub use self::solana_token2022_preflight::inspect_token2022_preflight;
/// Migrated MAX_TOKEN2022_STATEFUL_ACCOUNT_BYTES contract.
pub use self::solana_token2022_stateful::MAX_TOKEN2022_STATEFUL_ACCOUNT_BYTES;
/// Migrated Token2022StatefulContext contract.
pub use self::solana_token2022_stateful::Token2022StatefulContext;
/// Migrated Token2022StatefulReadRequest contract.
pub use self::solana_token2022_stateful::Token2022StatefulReadRequest;
/// Migrated Token2022StatefulReadResult contract.
pub use self::solana_token2022_stateful::Token2022StatefulReadResult;
/// Migrated Token2022StatefulSnapshotBundle contract.
pub use self::solana_token2022_stateful::Token2022StatefulSnapshotBundle;
/// Migrated materialize_parsed_token2022_stateful_snapshot contract.
pub use self::solana_token2022_stateful::materialize_parsed_token2022_stateful_snapshot;
/// Migrated materialize_parsed_token2022_stateful_snapshot_with_context contract.
pub use self::solana_token2022_stateful::materialize_parsed_token2022_stateful_snapshot_with_context;
/// Migrated materialize_token2022_account_info_result contract.
pub use self::solana_token2022_stateful::materialize_token2022_account_info_result;
/// Migrated materialize_token2022_stateful_snapshot contract.
pub use self::solana_token2022_stateful::materialize_token2022_stateful_snapshot;
/// Migrated read_token2022_stateful_snapshot contract.
pub use self::solana_token2022_stateful::read_token2022_stateful_snapshot;
/// Maximum number of evidence records accepted in one validation report. /// Maximum number of evidence records accepted in one validation report.
pub use self::solana_token2022_validation::MAX_TOKEN2022_VALIDATION_EVIDENCE; pub use self::solana_token2022_validation::MAX_TOKEN2022_VALIDATION_EVIDENCE;
/// Required validation environment for one Token-2022 scenario. /// Required validation environment for one Token-2022 scenario.

View File

@@ -0,0 +1,393 @@
// file: kb-pipeline/src/solana_elgamal_registry_stateful.rs
// version: 3
//! Contextual SPL ElGamal registry account-state validation and materialization routing.
use std::str::FromStr; // rust-rules: trait-import
/// Exact byte length of one SPL ElGamal registry account.
pub const ELGAMAL_REGISTRY_STATEFUL_ACCOUNT_BYTES: usize = 64;
/// One bounded SPL ElGamal registry RPC read request.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ElGamalRegistryStatefulReadRequest {
/// Endpoint role used for the HTTP RPC request.
pub query_role: std::string::String,
/// Canonical registry PDA.
pub registry_account: kb_lib::MdPubkey,
/// Optional minimum RPC context slot.
pub min_context_slot: std::option::Option<u64>,
}
/// One bounded registry RPC read and validated administrative projection.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct ElGamalRegistryStatefulReadResult {
/// Commitment used for the RPC read.
pub commitment: std::string::String,
/// Context slot returned by the endpoint.
pub context_slot: u64,
/// Complete validated registry snapshot.
pub snapshot: crate::ElGamalRegistryStatefulSnapshot,
}
/// Reads, validates, and routes one exact SPL ElGamal registry account.
pub async fn read_elgamal_registry_stateful_snapshot(
pool: &kb_onchain_transport::HttpEndpointPool,
request: &crate::ElGamalRegistryStatefulReadRequest,
) -> kb_core::Result<crate::ElGamalRegistryStatefulReadResult> {
if request.query_role.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::config(
"SPL ElGamal registry stateful read query_role must not be empty",
));
}
let config = match kb_onchain_transport::GetAccountInfoConfig::new_with_data(
kb_onchain_transport::RpcCommitmentLevel::Confirmed,
request.min_context_slot,
crate::ELGAMAL_REGISTRY_STATEFUL_ACCOUNT_BYTES,
) {
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(), &request.registry_account, &config)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return crate::materialize_elgamal_registry_account_info_result(request, &result);
}
/// Validates one complete RPC registry response before parsing and routing.
pub fn materialize_elgamal_registry_account_info_result(
request: &crate::ElGamalRegistryStatefulReadRequest,
result: &kb_onchain_transport::AccountInfoResult,
) -> kb_core::Result<crate::ElGamalRegistryStatefulReadResult> {
if let std::option::Option::Some(min_context_slot) = request.min_context_slot {
if result.context.slot < min_context_slot {
return std::result::Result::Err(kb_core::Error::new(
"elgamal_registry_stateful_context_slot_too_old",
format!(
"SPL ElGamal registry context slot {} is below requested minimum {min_context_slot}",
result.context.slot
),
));
}
}
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(
"elgamal_registry_stateful_account_missing",
format!(
"SPL ElGamal registry account {} does not exist",
request.registry_account.0
),
));
},
};
if account.executable {
return std::result::Result::Err(kb_core::Error::new(
"elgamal_registry_stateful_account_executable",
format!(
"SPL ElGamal registry state account {} must not be executable",
request.registry_account.0
),
));
}
if account.owner.0 != kb_program_ids::SPL_TOKEN2022_ELGAMAL_REGISTRY_PROGRAM_ID {
return std::result::Result::Err(kb_core::Error::new(
"elgamal_registry_stateful_owner_mismatch",
format!(
"SPL ElGamal registry owner must be {}, got {}",
kb_program_ids::SPL_TOKEN2022_ELGAMAL_REGISTRY_PROGRAM_ID,
account.owner.0
),
));
}
if account.space != crate::ELGAMAL_REGISTRY_STATEFUL_ACCOUNT_BYTES as u64
|| account.data.len() != crate::ELGAMAL_REGISTRY_STATEFUL_ACCOUNT_BYTES
{
return std::result::Result::Err(kb_core::Error::new(
"elgamal_registry_stateful_account_length_invalid",
format!(
"SPL ElGamal registry account must report and return exactly {} bytes, got space {} and data {}",
crate::ELGAMAL_REGISTRY_STATEFUL_ACCOUNT_BYTES,
account.space,
account.data.len()
),
));
}
let snapshot = match crate::materialize_elgamal_registry_stateful_snapshot(
request.registry_account.0.as_str(),
account.owner.0.as_str(),
result.context.slot,
account.data.as_slice(),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::new(
"elgamal_registry_stateful_projection_failed",
error,
));
},
};
return std::result::Result::Ok(crate::ElGamalRegistryStatefulReadResult {
commitment: "confirmed".to_string(),
context_slot: result.context.slot,
snapshot,
});
}
/// One contextually validated SPL ElGamal registry state snapshot.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct ElGamalRegistryStatefulSnapshot {
/// Canonical registry PDA identity.
pub registry_account_key: std::string::String,
/// Wallet address stored as the registry owner.
pub owner: std::string::String,
/// Context slot associated with the account read.
pub slot: u64,
/// Processor-owned administrative projection.
pub output: kb_lib::MtApiMaterializedOutput,
}
/// Parse, validate the official owner-derived PDA, and materialize one registry snapshot.
pub fn materialize_elgamal_registry_stateful_snapshot(
registry_account_key: &str,
owner_program_id: &str,
slot: u64,
data: &[u8],
) -> std::result::Result<crate::ElGamalRegistryStatefulSnapshot, String> {
if owner_program_id != kb_program_ids::SPL_TOKEN2022_ELGAMAL_REGISTRY_PROGRAM_ID {
return std::result::Result::Err(format!(
"SPL ElGamal registry snapshot owner must be {}, got {owner_program_id}",
kb_program_ids::SPL_TOKEN2022_ELGAMAL_REGISTRY_PROGRAM_ID
));
}
let state = match kb_lib::decoder_spl_elgamal_registry_parse_elgamal_registry_state(data) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let owner = match solana_pubkey::Pubkey::from_str(state.owner.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(format!(
"SPL ElGamal registry owner is not a valid address: {error}"
));
},
};
let program_id = match solana_pubkey::Pubkey::from_str(
kb_program_ids::SPL_TOKEN2022_ELGAMAL_REGISTRY_PROGRAM_ID,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(format!(
"Configured SPL ElGamal registry program ID is invalid: {error}"
));
},
};
let expected =
spl_elgamal_registry_interface::get_elgamal_registry_address(&owner, &program_id);
if expected.to_string() != registry_account_key {
return std::result::Result::Err(format!(
"SPL ElGamal registry account {registry_account_key} does not match owner-derived PDA {expected}"
));
}
let output = match kb_lib::materializer_admin_materialize_elgamal_registry_state_snapshot(
registry_account_key,
slot,
&state,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(crate::ElGamalRegistryStatefulSnapshot {
registry_account_key: registry_account_key.to_string(),
owner: state.owner,
slot,
output,
});
}
#[cfg(test)]
mod tests {
use std::str::FromStr; // rust-rules: trait-import
fn registry_fixture(owner: &solana_pubkey::Pubkey) -> (std::string::String, [u8; 64]) {
let program_id = match solana_pubkey::Pubkey::from_str(
kb_program_ids::SPL_TOKEN2022_ELGAMAL_REGISTRY_PROGRAM_ID,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => solana_pubkey::Pubkey::default(),
};
let registry =
spl_elgamal_registry_interface::get_elgamal_registry_address(owner, &program_id);
let mut data = [0u8; 64];
data[..32].copy_from_slice(owner.as_ref());
data[32..].copy_from_slice(&[9u8; 32]);
return (registry.to_string(), data);
}
#[test]
fn exact_owner_derived_registry_pda_routes_one_admin_snapshot() {
let owner = solana_pubkey::Pubkey::new_from_array([7u8; 32]);
let (registry, data) = registry_fixture(&owner);
let snapshot = crate::materialize_elgamal_registry_stateful_snapshot(
registry.as_str(),
kb_program_ids::SPL_TOKEN2022_ELGAMAL_REGISTRY_PROGRAM_ID,
55,
&data,
);
assert_eq!(
snapshot.as_ref().map(|value| return value.owner.clone()),
std::result::Result::Ok(owner.to_string())
);
assert_eq!(
snapshot
.as_ref()
.map(|value| return value.output.payload_json["finalAccountStateCaptured"].clone()),
std::result::Result::Ok(serde_json::json!(true))
);
}
#[test]
fn foreign_owner_program_wrong_pda_and_invalid_length_fail_closed() {
let owner = solana_pubkey::Pubkey::new_from_array([8u8; 32]);
let (registry, data) = registry_fixture(&owner);
let foreign = crate::materialize_elgamal_registry_stateful_snapshot(
registry.as_str(),
kb_program_ids::SPL_TOKEN2022_PROGRAM_ID,
55,
&data,
);
assert!(foreign.is_err());
let wrong = crate::materialize_elgamal_registry_stateful_snapshot(
solana_pubkey::Pubkey::new_from_array([10u8; 32]).to_string().as_str(),
kb_program_ids::SPL_TOKEN2022_ELGAMAL_REGISTRY_PROGRAM_ID,
55,
&data,
);
assert!(wrong.is_err());
let invalid = crate::materialize_elgamal_registry_stateful_snapshot(
registry.as_str(),
kb_program_ids::SPL_TOKEN2022_ELGAMAL_REGISTRY_PROGRAM_ID,
55,
&data[..63],
);
assert!(invalid.is_err());
}
#[test]
fn exact_rpc_registry_routes_after_context_owner_and_length_validation() {
let owner = solana_pubkey::Pubkey::new_from_array([13u8; 32]);
let (registry, data) = registry_fixture(&owner);
let request = crate::ElGamalRegistryStatefulReadRequest {
query_role: "execution".to_string(),
registry_account: kb_lib::MdPubkey(registry),
min_context_slot: std::option::Option::Some(70),
};
let result = kb_onchain_transport::AccountInfoResult {
context: kb_onchain_transport::RpcResponseContext {
slot: 71,
api_version: std::option::Option::None,
},
account: std::option::Option::Some(kb_onchain_transport::AccountInfoValue {
lamports: 1,
owner: kb_lib::MdProgramId(
kb_program_ids::SPL_TOKEN2022_ELGAMAL_REGISTRY_PROGRAM_ID.to_string(),
),
executable: false,
rent_epoch: 0,
space: 64,
data: data.to_vec(),
}),
};
let materialized =
crate::materialize_elgamal_registry_account_info_result(&request, &result);
assert_eq!(
materialized.as_ref().map(|value| return value.context_slot),
std::result::Result::Ok(71)
);
assert_eq!(
materialized.as_ref().map(|value| return value.snapshot.owner.clone()),
std::result::Result::Ok(owner.to_string())
);
}
#[test]
fn stale_missing_foreign_executable_and_wrong_length_registry_reads_fail_closed() {
let owner = solana_pubkey::Pubkey::new_from_array([14u8; 32]);
let (registry, data) = registry_fixture(&owner);
let request = crate::ElGamalRegistryStatefulReadRequest {
query_role: "execution".to_string(),
registry_account: kb_lib::MdPubkey(registry),
min_context_slot: std::option::Option::Some(80),
};
let base = kb_onchain_transport::AccountInfoValue {
lamports: 1,
owner: kb_lib::MdProgramId(
kb_program_ids::SPL_TOKEN2022_ELGAMAL_REGISTRY_PROGRAM_ID.to_string(),
),
executable: false,
rent_epoch: 0,
space: 64,
data: data.to_vec(),
};
let stale = kb_onchain_transport::AccountInfoResult {
context: kb_onchain_transport::RpcResponseContext {
slot: 79,
api_version: std::option::Option::None,
},
account: std::option::Option::Some(base.clone()),
};
assert!(crate::materialize_elgamal_registry_account_info_result(&request, &stale).is_err());
let missing = kb_onchain_transport::AccountInfoResult {
context: kb_onchain_transport::RpcResponseContext {
slot: 80,
api_version: std::option::Option::None,
},
account: std::option::Option::None,
};
assert!(
crate::materialize_elgamal_registry_account_info_result(&request, &missing).is_err()
);
let mut foreign = base.clone();
foreign.owner = kb_lib::MdProgramId(kb_program_ids::SPL_TOKEN2022_PROGRAM_ID.to_string());
let foreign = kb_onchain_transport::AccountInfoResult {
context: kb_onchain_transport::RpcResponseContext {
slot: 80,
api_version: std::option::Option::None,
},
account: std::option::Option::Some(foreign),
};
assert!(
crate::materialize_elgamal_registry_account_info_result(&request, &foreign).is_err()
);
let mut executable = base.clone();
executable.executable = true;
let executable = kb_onchain_transport::AccountInfoResult {
context: kb_onchain_transport::RpcResponseContext {
slot: 80,
api_version: std::option::Option::None,
},
account: std::option::Option::Some(executable),
};
assert!(
crate::materialize_elgamal_registry_account_info_result(&request, &executable).is_err()
);
let mut wrong_length = base;
wrong_length.data.pop();
let wrong_length = kb_onchain_transport::AccountInfoResult {
context: kb_onchain_transport::RpcResponseContext {
slot: 80,
api_version: std::option::Option::None,
},
account: std::option::Option::Some(wrong_length),
};
assert!(
crate::materialize_elgamal_registry_account_info_result(&request, &wrong_length)
.is_err()
);
}
}

View File

@@ -0,0 +1,429 @@
// file: kb-pipeline/src/solana_token2022_preflight.rs
// version: 2
//! Bounded Token-2022 stateful preflight orchestration.
/// Maximum distinct Token-2022 accounts accepted by one preflight inspection.
pub const MAX_TOKEN2022_PREFLIGHT_ACCOUNTS: usize = 16;
/// Maximum aggregate account-data budget accepted by one preflight inspection.
pub const MAX_TOKEN2022_PREFLIGHT_TOTAL_BYTES: usize = 262_144;
/// One exact Token-2022 account requirement for a stateful preflight.
#[derive(Clone, Debug, PartialEq)]
pub struct Token2022PreflightRequirement {
/// Stable semantic role used in diagnostics.
pub role: std::string::String,
/// Canonical account address.
pub account: kb_lib::MdPubkey,
/// Expected Token-2022 state category.
pub kind: kb_lib::DcToken2022StateKind,
/// Maximum complete account bytes accepted for this requirement.
pub max_data_bytes: usize,
/// Optional expected mint for a Token Account.
pub expected_mint: std::option::Option<kb_lib::MdPubkey>,
/// Optional expected owner for a Token Account.
pub expected_owner: std::option::Option<kb_lib::MdPubkey>,
/// Optional exact decimals expected for a Mint.
pub expected_decimals: std::option::Option<u8>,
/// Published extension names required on the account.
pub required_extensions: std::vec::Vec<std::string::String>,
/// Optional external identities needed by cross-account extensions.
pub context: crate::Token2022StatefulContext,
}
/// One bounded Token-2022 preflight request.
#[derive(Clone, Debug, PartialEq)]
pub struct Token2022PreflightRequest {
/// Endpoint role used by all RPC reads.
pub query_role: std::string::String,
/// Optional minimum context slot shared by all reads.
pub min_context_slot: std::option::Option<u64>,
/// Maximum distinct accounts accepted after deduplication.
pub max_accounts: usize,
/// Maximum aggregate requested account bytes.
pub max_total_data_bytes: usize,
/// Ordered account requirements.
pub requirements: std::vec::Vec<Token2022PreflightRequirement>,
/// Optional ElGamal registry read required by the operation.
pub elgamal_registry: std::option::Option<crate::ElGamalRegistryStatefulReadRequest>,
}
/// One validated account result in a Token-2022 preflight report.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct Token2022PreflightAccountReport {
/// Stable semantic role.
pub role: std::string::String,
/// Canonical account address.
pub account: kb_lib::MdPubkey,
/// State category observed after parsing.
pub state_kind: std::string::String,
/// Context slot returned by the endpoint.
pub context_slot: u64,
/// Ordered extension names observed on the account.
pub extension_names: std::vec::Vec<std::string::String>,
/// Ordered successful semantic checks.
pub checks: std::vec::Vec<std::string::String>,
}
/// Complete bounded Token-2022 stateful preflight report.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct Token2022PreflightReport {
/// Commitment used for all account reads.
pub commitment: std::string::String,
/// Highest context slot observed across all reads.
pub context_slot: u64,
/// Aggregate requested account-data budget.
pub requested_data_bytes: usize,
/// Ordered distinct Token-2022 account reports.
pub accounts: std::vec::Vec<Token2022PreflightAccountReport>,
/// Whether an ElGamal registry was required and validated.
pub elgamal_registry_validated: bool,
}
/// Inspects all bounded Token-2022 state required before simulation.
pub async fn inspect_token2022_preflight(
pool: &kb_onchain_transport::HttpEndpointPool,
request: &crate::Token2022PreflightRequest,
) -> kb_core::Result<crate::Token2022PreflightReport> {
let requirements = match validate_and_deduplicate_requirements(request) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let requested_data_bytes = requirements
.iter()
.fold(0usize, |total, requirement| {
return total.saturating_add(requirement.max_data_bytes);
})
.saturating_add(if request.elgamal_registry.is_some() {
crate::ELGAMAL_REGISTRY_STATEFUL_ACCOUNT_BYTES
} else {
0
});
let mut context_slot = request.min_context_slot.unwrap_or(0);
let mut accounts = std::vec::Vec::with_capacity(requirements.len());
for requirement in requirements {
let read_request = crate::Token2022StatefulReadRequest {
query_role: request.query_role.clone(),
account: requirement.account.clone(),
kind: requirement.kind,
min_context_slot: request.min_context_slot,
max_data_bytes: requirement.max_data_bytes,
context: requirement.context.clone(),
};
let read_result = match crate::read_token2022_stateful_snapshot(pool, &read_request).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let account_report = match validate_snapshot_requirement(&requirement, &read_result) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
context_slot = context_slot.max(account_report.context_slot);
accounts.push(account_report);
}
let mut elgamal_registry_validated = false;
if let std::option::Option::Some(registry_request) = request.elgamal_registry.as_ref() {
if registry_request.query_role != request.query_role {
return std::result::Result::Err(kb_core::Error::config(
"Token-2022 preflight ElGamal registry query role must match the shared query role",
));
}
let registry_result =
match crate::read_elgamal_registry_stateful_snapshot(pool, registry_request).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
context_slot = context_slot.max(registry_result.context_slot);
elgamal_registry_validated = true;
}
return std::result::Result::Ok(crate::Token2022PreflightReport {
commitment: "confirmed".to_string(),
context_slot,
requested_data_bytes,
accounts,
elgamal_registry_validated,
});
}
fn validate_and_deduplicate_requirements(
request: &crate::Token2022PreflightRequest,
) -> kb_core::Result<std::vec::Vec<crate::Token2022PreflightRequirement>> {
if request.query_role.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::config(
"Token-2022 preflight query_role must not be empty",
));
}
if request.max_accounts == 0 || request.max_accounts > crate::MAX_TOKEN2022_PREFLIGHT_ACCOUNTS {
return std::result::Result::Err(kb_core::Error::config(format!(
"Token-2022 preflight max_accounts must be between 1 and {}",
crate::MAX_TOKEN2022_PREFLIGHT_ACCOUNTS
)));
}
if request.max_total_data_bytes == 0
|| request.max_total_data_bytes > crate::MAX_TOKEN2022_PREFLIGHT_TOTAL_BYTES
{
return std::result::Result::Err(kb_core::Error::config(format!(
"Token-2022 preflight max_total_data_bytes must be between 1 and {}",
crate::MAX_TOKEN2022_PREFLIGHT_TOTAL_BYTES
)));
}
if request.requirements.is_empty() {
return std::result::Result::Err(kb_core::Error::config(
"Token-2022 preflight requires at least one account",
));
}
let mut indexes = std::collections::BTreeMap::<std::string::String, usize>::new();
let mut unique = std::vec::Vec::<crate::Token2022PreflightRequirement>::new();
for requirement in &request.requirements {
if requirement.role.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::config(
"Token-2022 preflight requirement role must not be empty",
));
}
if requirement.max_data_bytes == 0
|| requirement.max_data_bytes > crate::MAX_TOKEN2022_STATEFUL_ACCOUNT_BYTES
{
return std::result::Result::Err(kb_core::Error::config(format!(
"Token-2022 preflight account {} has invalid max_data_bytes {}",
requirement.account.0, requirement.max_data_bytes
)));
}
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_preflight_conflicting_duplicate",
format!(
"Token-2022 preflight account {} has conflicting requirements",
requirement.account.0
),
));
}
continue;
}
indexes.insert(requirement.account.0.clone(), unique.len());
unique.push(requirement.clone());
}
let total_accounts =
unique
.len()
.saturating_add(if request.elgamal_registry.is_some() { 1 } else { 0 });
if total_accounts > request.max_accounts {
return std::result::Result::Err(kb_core::Error::new(
"token2022_preflight_account_limit_exceeded",
format!(
"Token-2022 preflight requires {total_accounts} distinct accounts above limit {}",
request.max_accounts
),
));
}
let requested_data_bytes = unique
.iter()
.fold(0usize, |total, requirement| {
return total.saturating_add(requirement.max_data_bytes);
})
.saturating_add(if request.elgamal_registry.is_some() {
crate::ELGAMAL_REGISTRY_STATEFUL_ACCOUNT_BYTES
} else {
0
});
if requested_data_bytes > request.max_total_data_bytes {
return std::result::Result::Err(kb_core::Error::new(
"token2022_preflight_data_budget_exceeded",
format!(
"Token-2022 preflight requests {requested_data_bytes} bytes above aggregate limit {}",
request.max_total_data_bytes
),
));
}
return std::result::Result::Ok(unique);
}
fn validate_snapshot_requirement(
requirement: &crate::Token2022PreflightRequirement,
result: &crate::Token2022StatefulReadResult,
) -> kb_core::Result<crate::Token2022PreflightAccountReport> {
let token_output = result
.snapshot
.outputs
.iter()
.find(|output| return output.payload_json["domain"] == "spl_token2022_account_state");
let token_output = match token_output {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(kb_core::Error::invalid_state(
"Token-2022 preflight snapshot is missing its token account owner projection",
));
},
};
let base_fields = &token_output.payload_json["baseFields"];
let mut checks = std::vec![
"owner_program_id".to_string(),
"complete_account_data".to_string(),
"state_kind".to_string()
];
if let std::option::Option::Some(expected_mint) = requirement.expected_mint.as_ref() {
if base_fields["mint"].as_str() != std::option::Option::Some(expected_mint.0.as_str()) {
return std::result::Result::Err(kb_core::Error::new(
"token2022_preflight_mint_mismatch",
format!(
"Token-2022 account {} mint does not match expected {}",
requirement.account.0, expected_mint.0
),
));
}
checks.push("mint_identity".to_string());
}
if let std::option::Option::Some(expected_owner) = requirement.expected_owner.as_ref() {
if base_fields["owner"].as_str() != std::option::Option::Some(expected_owner.0.as_str()) {
return std::result::Result::Err(kb_core::Error::new(
"token2022_preflight_owner_mismatch",
format!(
"Token-2022 account {} owner does not match expected {}",
requirement.account.0, expected_owner.0
),
));
}
checks.push("token_account_owner".to_string());
}
if let std::option::Option::Some(expected_decimals) = requirement.expected_decimals {
if base_fields["decimals"].as_u64() != std::option::Option::Some(expected_decimals as u64) {
return std::result::Result::Err(kb_core::Error::new(
"token2022_preflight_decimals_mismatch",
format!(
"Token-2022 mint {} decimals do not match expected {expected_decimals}",
requirement.account.0
),
));
}
checks.push("mint_decimals".to_string());
}
for required_extension in &requirement.required_extensions {
if !result
.snapshot
.extension_names
.iter()
.any(|value| return value == required_extension)
{
return std::result::Result::Err(kb_core::Error::new(
"token2022_preflight_extension_missing",
format!(
"Token-2022 account {} is missing required extension {required_extension}",
requirement.account.0
),
));
}
}
if !requirement.required_extensions.is_empty() {
checks.push("required_extensions".to_string());
}
return std::result::Result::Ok(crate::Token2022PreflightAccountReport {
role: requirement.role.clone(),
account: requirement.account.clone(),
state_kind: result.snapshot.state_kind.clone(),
context_slot: result.context_slot,
extension_names: result.snapshot.extension_names.clone(),
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) -> crate::Token2022PreflightRequirement {
return crate::Token2022PreflightRequirement {
role: "source".to_string(),
account,
kind: kb_lib::DcToken2022StateKind::Account,
max_data_bytes: 512,
expected_mint: std::option::Option::None,
expected_owner: std::option::Option::None,
expected_decimals: std::option::Option::None,
required_extensions: std::vec::Vec::new(),
context: crate::Token2022StatefulContext::default(),
};
}
#[test]
fn exact_duplicate_requirements_are_deduplicated_and_bounded() {
let item = requirement(pubkey(1));
let request = crate::Token2022PreflightRequest {
query_role: "query".to_string(),
min_context_slot: std::option::Option::Some(7),
max_accounts: 1,
max_total_data_bytes: 512,
requirements: std::vec![item.clone(), item],
elgamal_registry: std::option::Option::None,
};
let result = super::validate_and_deduplicate_requirements(&request);
assert_eq!(result.as_ref().map(std::vec::Vec::len), std::result::Result::Ok(1));
}
#[test]
fn conflicting_duplicates_and_aggregate_budget_fail_closed() {
let first = requirement(pubkey(2));
let mut conflicting = first.clone();
conflicting.role = "destination".to_string();
let conflict_request = crate::Token2022PreflightRequest {
query_role: "query".to_string(),
min_context_slot: std::option::Option::None,
max_accounts: 2,
max_total_data_bytes: 1024,
requirements: std::vec![first, conflicting],
elgamal_registry: std::option::Option::None,
};
assert!(super::validate_and_deduplicate_requirements(&conflict_request).is_err());
let budget_request = crate::Token2022PreflightRequest {
query_role: "query".to_string(),
min_context_slot: std::option::Option::None,
max_accounts: 2,
max_total_data_bytes: 700,
requirements: std::vec![requirement(pubkey(3)), requirement(pubkey(4))],
elgamal_registry: std::option::Option::None,
};
assert!(super::validate_and_deduplicate_requirements(&budget_request).is_err());
}
#[test]
fn requirement_validation_checks_mint_owner_decimals_and_extensions() {
let account = pubkey(5);
let mint = pubkey(6);
let owner = pubkey(7);
let requirement = crate::Token2022PreflightRequirement {
role: "source".to_string(),
account: account.clone(),
kind: kb_lib::DcToken2022StateKind::Account,
max_data_bytes: 512,
expected_mint: std::option::Option::Some(mint.clone()),
expected_owner: std::option::Option::Some(owner.clone()),
expected_decimals: std::option::Option::None,
required_extensions: std::vec!["memo_transfer".to_string()],
context: crate::Token2022StatefulContext::default(),
};
let result = crate::Token2022StatefulReadResult {
commitment: "confirmed".to_string(),
context_slot: 42,
snapshot: crate::Token2022StatefulSnapshotBundle {
account_key: account.0.clone(),
slot: 42,
state_kind: "account".to_string(),
extension_names: std::vec!["memo_transfer".to_string()],
outputs: std::vec![kb_lib::MtApiMaterializedOutput {
output_key: "state".to_string(),
family: kb_lib::MdMaterializedEventFamily::TokenAccount,
payload_json: serde_json::json!({
"domain":"spl_token2022_account_state",
"baseFields":{"mint":mint.0,"owner":owner.0}
}),
}],
},
};
let report = super::validate_snapshot_requirement(&requirement, &result);
assert_eq!(
report.as_ref().map(|value| return value.checks.len()),
std::result::Result::Ok(6)
);
}
}

View File

@@ -0,0 +1,742 @@
// file: kb-pipeline/src/solana_token2022_stateful.rs
// version: 5
//! Contextual Token-2022 account-state validation and materialization routing.
/// Optional external identities required to validate cross-account Token-2022 state.
#[derive(Clone, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct Token2022StatefulContext {
/// Expected group account for one TokenGroupMember extension.
pub expected_group_address: std::option::Option<std::string::String>,
}
/// Maximum complete Token-2022 account data accepted by one bounded RPC read.
pub const MAX_TOKEN2022_STATEFUL_ACCOUNT_BYTES: usize = 65_536;
/// One bounded Token-2022 account read request.
#[derive(Clone, Debug, PartialEq)]
pub struct Token2022StatefulReadRequest {
/// Endpoint role used for the HTTP RPC request.
pub query_role: std::string::String,
/// Canonical account address.
pub account: kb_lib::MdPubkey,
/// Expected Token-2022 base-state category.
pub kind: kb_lib::DcToken2022StateKind,
/// Optional minimum RPC context slot.
pub min_context_slot: std::option::Option<u64>,
/// Maximum decoded account bytes accepted from the endpoint.
pub max_data_bytes: usize,
/// Optional external identities needed by cross-account extensions.
pub context: crate::Token2022StatefulContext,
}
/// One bounded RPC read and its contextually validated projections.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct Token2022StatefulReadResult {
/// Commitment used for the RPC read.
pub commitment: std::string::String,
/// Context slot returned by the endpoint.
pub context_slot: u64,
/// Complete validated snapshot bundle.
pub snapshot: crate::Token2022StatefulSnapshotBundle,
}
/// Reads, validates, parses, and routes one bounded Token-2022 account snapshot.
pub async fn read_token2022_stateful_snapshot(
pool: &kb_onchain_transport::HttpEndpointPool,
request: &crate::Token2022StatefulReadRequest,
) -> kb_core::Result<crate::Token2022StatefulReadResult> {
if request.query_role.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::config(
"Token-2022 stateful read query_role must not be empty",
));
}
if request.max_data_bytes == 0
|| request.max_data_bytes > crate::MAX_TOKEN2022_STATEFUL_ACCOUNT_BYTES
{
return std::result::Result::Err(kb_core::Error::config(format!(
"Token-2022 stateful read max_data_bytes must be between 1 and {}",
crate::MAX_TOKEN2022_STATEFUL_ACCOUNT_BYTES
)));
}
let config = match kb_onchain_transport::GetAccountInfoConfig::new_with_data(
kb_onchain_transport::RpcCommitmentLevel::Confirmed,
request.min_context_slot,
request.max_data_bytes,
) {
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(), &request.account, &config)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return crate::materialize_token2022_account_info_result(request, &result);
}
/// Validates one complete RPC account response before Token-2022 parsing and routing.
pub fn materialize_token2022_account_info_result(
request: &crate::Token2022StatefulReadRequest,
result: &kb_onchain_transport::AccountInfoResult,
) -> kb_core::Result<crate::Token2022StatefulReadResult> {
if request.max_data_bytes == 0
|| request.max_data_bytes > crate::MAX_TOKEN2022_STATEFUL_ACCOUNT_BYTES
{
return std::result::Result::Err(kb_core::Error::config(format!(
"Token-2022 stateful read max_data_bytes must be between 1 and {}",
crate::MAX_TOKEN2022_STATEFUL_ACCOUNT_BYTES
)));
}
if let std::option::Option::Some(min_context_slot) = request.min_context_slot {
if result.context.slot < min_context_slot {
return std::result::Result::Err(kb_core::Error::new(
"token2022_stateful_context_slot_too_old",
format!(
"Token-2022 account context slot {} is below requested minimum {min_context_slot}",
result.context.slot
),
));
}
}
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_stateful_account_missing",
format!("Token-2022 account {} does not exist", request.account.0),
));
},
};
if account.executable {
return std::result::Result::Err(kb_core::Error::new(
"token2022_stateful_account_executable",
format!("Token-2022 state account {} must not be executable", request.account.0),
));
}
if account.owner.0 != kb_program_ids::SPL_TOKEN2022_PROGRAM_ID {
return std::result::Result::Err(kb_core::Error::new(
"token2022_stateful_owner_mismatch",
format!(
"Token-2022 state account {} owner must be {}, got {}",
request.account.0,
kb_program_ids::SPL_TOKEN2022_PROGRAM_ID,
account.owner.0
),
));
}
if account.space > request.max_data_bytes as u64 || account.data.len() > request.max_data_bytes
{
return std::result::Result::Err(kb_core::Error::new(
"token2022_stateful_account_too_large",
format!(
"Token-2022 account {} reports {} bytes and returned {} bytes above limit {}",
request.account.0,
account.space,
account.data.len(),
request.max_data_bytes
),
));
}
if account.space != account.data.len() as u64 {
return std::result::Result::Err(kb_core::Error::new(
"token2022_stateful_account_data_incomplete",
format!(
"Token-2022 account {} reports {} bytes but returned {} decoded bytes",
request.account.0,
account.space,
account.data.len()
),
));
}
let state = match kb_lib::decoder_spl_token2022_parse_token2022_state(
request.kind,
account.data.as_slice(),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::new(
"token2022_stateful_parse_failed",
error,
));
},
};
let snapshot = match crate::materialize_parsed_token2022_stateful_snapshot_with_context(
request.account.0.as_str(),
result.context.slot,
&state,
&request.context,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::new(
"token2022_stateful_projection_failed",
error,
));
},
};
return std::result::Result::Ok(crate::Token2022StatefulReadResult {
commitment: "confirmed".to_string(),
context_slot: result.context.slot,
snapshot,
});
}
/// One contextually validated Token-2022 state snapshot and its owned projections.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct Token2022StatefulSnapshotBundle {
/// Canonical account identity used for contextual checks and stable output keys.
pub account_key: std::string::String,
/// Context slot associated with the account read.
pub slot: u64,
/// Parsed base-state category.
pub state_kind: std::string::String,
/// Ordered published or future extension names retained by the parser.
pub extension_names: std::vec::Vec<std::string::String>,
/// Processor-owned projections routed without duplicate ownership.
pub outputs: std::vec::Vec<kb_lib::MtApiMaterializedOutput>,
}
/// Parse, contextually validate, and materialize one bounded Token-2022 account snapshot.
pub fn materialize_token2022_stateful_snapshot(
account_key: &str,
owner_program_id: &str,
slot: u64,
kind: kb_lib::DcToken2022StateKind,
data: &[u8],
) -> std::result::Result<crate::Token2022StatefulSnapshotBundle, String> {
if owner_program_id != kb_program_ids::SPL_TOKEN2022_PROGRAM_ID {
return std::result::Result::Err(format!(
"Token-2022 state snapshot owner must be {}, got {owner_program_id}",
kb_program_ids::SPL_TOKEN2022_PROGRAM_ID
));
}
let state = match kb_lib::decoder_spl_token2022_parse_token2022_state(kind, data) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return crate::materialize_parsed_token2022_stateful_snapshot_with_context(
account_key,
slot,
&state,
&crate::Token2022StatefulContext::default(),
);
}
/// Contextually validate and materialize one already parsed Token-2022 account snapshot.
pub fn materialize_parsed_token2022_stateful_snapshot(
account_key: &str,
slot: u64,
state: &kb_lib::DcToken2022State,
) -> std::result::Result<crate::Token2022StatefulSnapshotBundle, String> {
return crate::materialize_parsed_token2022_stateful_snapshot_with_context(
account_key,
slot,
state,
&crate::Token2022StatefulContext::default(),
);
}
/// Contextually validate one parsed Token-2022 snapshot with external cross-account identities.
pub fn materialize_parsed_token2022_stateful_snapshot_with_context(
account_key: &str,
slot: u64,
state: &kb_lib::DcToken2022State,
context: &crate::Token2022StatefulContext,
) -> std::result::Result<crate::Token2022StatefulSnapshotBundle, String> {
if account_key.trim().is_empty() {
return std::result::Result::Err(
"Token-2022 stateful snapshot requires a non-empty account key".to_string(),
);
}
let decoded = match bs58::decode(account_key).into_vec() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(
"Token-2022 stateful snapshot account key must be valid base58".to_string(),
);
},
};
if decoded.len() != 32 {
return std::result::Result::Err(format!(
"Token-2022 stateful snapshot account key must decode to 32 bytes, got {}",
decoded.len()
));
}
match validate_embedded_mint_identity(account_key, state) {
std::result::Result::Ok(()) => {},
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
match validate_group_member_identity(state, context) {
std::result::Result::Ok(()) => {},
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
let account_output = match kb_lib::materializer_token_materialize_token2022_state_snapshot(
account_key,
slot,
state,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut outputs = std::vec![account_output];
if state.kind == kb_lib::DcToken2022StateKind::Mint {
let metadata_outputs = match kb_lib::materializer_metadata_materialize_token2022_snapshot(
account_key,
slot,
state,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
outputs.extend(metadata_outputs);
}
let fee_outputs = match kb_lib::materializer_fees_materialize_token2022_state_snapshots(
account_key,
slot,
state,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
outputs.extend(fee_outputs);
let admin_outputs = match kb_lib::materializer_admin_materialize_token2022_state_snapshots(
account_key,
slot,
state,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
outputs.extend(admin_outputs);
let state_kind = match state.kind {
kb_lib::DcToken2022StateKind::Mint => "mint",
kb_lib::DcToken2022StateKind::Account => "account",
kb_lib::DcToken2022StateKind::Multisig => "multisig",
};
return std::result::Result::Ok(crate::Token2022StatefulSnapshotBundle {
account_key: account_key.to_string(),
slot,
state_kind: state_kind.to_string(),
extension_names: state
.extensions
.iter()
.map(|entry| return entry.extension_name.to_string())
.collect(),
outputs,
});
}
fn validate_embedded_mint_identity(
account_key: &str,
state: &kb_lib::DcToken2022State,
) -> std::result::Result<(), String> {
for entry in &state.extensions {
if entry.extension_name != "token_metadata"
&& entry.extension_name != "token_group"
&& entry.extension_name != "token_group_member"
{
continue;
}
let embedded_mint = entry.value_fields.get("mint").and_then(serde_json::Value::as_str);
let embedded_mint = match embedded_mint {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(format!(
"Token-2022 {} extension requires a structured mint field",
entry.extension_name
));
},
};
if embedded_mint != account_key {
return std::result::Result::Err(format!(
"Token-2022 {} mint {} does not match account {account_key}",
entry.extension_name, embedded_mint
));
}
}
return std::result::Result::Ok(());
}
fn validate_group_member_identity(
state: &kb_lib::DcToken2022State,
context: &crate::Token2022StatefulContext,
) -> std::result::Result<(), String> {
for entry in &state.extensions {
if entry.extension_name != "token_group_member" {
continue;
}
let group = entry.value_fields.get("group").and_then(serde_json::Value::as_str);
let group = match group {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(
"Token-2022 token_group_member extension requires a structured group field"
.to_string(),
);
},
};
let expected = match context.expected_group_address.as_deref() {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(
"Token-2022 token_group_member validation requires an expected group address"
.to_string(),
);
},
};
if group != expected {
return std::result::Result::Err(format!(
"Token-2022 token_group_member group {group} does not match expected group {expected}"
));
}
}
return std::result::Result::Ok(());
}
#[cfg(test)]
mod tests {
fn account_key(byte: u8) -> String {
return bs58::encode([byte; 32]).into_string();
}
fn mint_state(account_key: &str) -> kb_lib::DcToken2022State {
return kb_lib::DcToken2022State {
kind: kb_lib::DcToken2022StateKind::Mint,
base_fields: serde_json::json!({"supply":"1","decimals":0,"initialized":true}),
base_hex: "00".repeat(82),
account_type: std::option::Option::Some(1),
extensions: std::vec![
kb_lib::DcToken2022TlvEntry {
extension_type: 19,
extension_name: "token_metadata",
value_hex: "01".to_string(),
value_fields: serde_json::json!({
"mint": account_key,
"name": "Token",
"symbol": "TOK",
"uri": "https://example.invalid/token.json"
}),
},
kb_lib::DcToken2022TlvEntry {
extension_type: 20,
extension_name: "token_group",
value_hex: "02".to_string(),
value_fields: serde_json::json!({
"mint": account_key,
"size": "1",
"maxSize": "10"
}),
},
],
};
}
#[test]
fn mint_snapshot_routes_account_and_metadata_outputs_once() {
let account_key = account_key(7);
let state = mint_state(account_key.as_str());
let bundle =
crate::materialize_parsed_token2022_stateful_snapshot(account_key.as_str(), 42, &state);
assert_eq!(
bundle.as_ref().map(|value| return value.outputs.len()),
std::result::Result::Ok(3)
);
assert_eq!(
bundle.as_ref().map(|value| return value.state_kind.clone()),
std::result::Result::Ok("mint".to_string())
);
assert_eq!(
bundle.as_ref().map(|value| return value.extension_names.clone()),
std::result::Result::Ok(std::vec![
"token_metadata".to_string(),
"token_group".to_string()
])
);
}
#[test]
fn embedded_metadata_mint_must_match_the_account_identity() {
let account_address = account_key(8);
let mut state = mint_state(account_address.as_str());
state.extensions[0].value_fields["mint"] = serde_json::json!(account_key(9));
let result = crate::materialize_parsed_token2022_stateful_snapshot(
account_address.as_str(),
42,
&state,
);
assert!(result.is_err());
}
#[test]
fn owner_and_account_identity_fail_closed_before_projection() {
let account_key = account_key(10);
let data = [0u8; 82];
let wrong_owner = crate::materialize_token2022_stateful_snapshot(
account_key.as_str(),
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
1,
kb_lib::DcToken2022StateKind::Mint,
&data,
);
assert!(wrong_owner.is_err());
let state = mint_state(account_key.as_str());
let malformed_key =
crate::materialize_parsed_token2022_stateful_snapshot("not-base58-0", 1, &state);
assert!(malformed_key.is_err());
}
#[test]
fn account_snapshot_has_no_metadata_projection() {
let account_key = account_key(11);
let state = kb_lib::DcToken2022State {
kind: kb_lib::DcToken2022StateKind::Account,
base_fields: serde_json::json!({"amount":"0","state":"initialized"}),
base_hex: "00".repeat(165),
account_type: std::option::Option::None,
extensions: std::vec::Vec::new(),
};
let bundle =
crate::materialize_parsed_token2022_stateful_snapshot(account_key.as_str(), 9, &state);
assert_eq!(
bundle.as_ref().map(|value| return value.outputs.len()),
std::result::Result::Ok(1)
);
}
#[test]
fn group_member_requires_and_matches_external_group_identity() {
let account_address = account_key(12);
let group_address = account_key(13);
let state = kb_lib::DcToken2022State {
kind: kb_lib::DcToken2022StateKind::Mint,
base_fields: serde_json::json!({"supply":"1","decimals":0,"initialized":true}),
base_hex: "00".repeat(82),
account_type: std::option::Option::Some(1),
extensions: std::vec![kb_lib::DcToken2022TlvEntry {
extension_type: 23,
extension_name: "token_group_member",
value_hex: "03".to_string(),
value_fields: serde_json::json!({
"mint": account_address,
"group": group_address,
"memberNumber": "1"
}),
}],
};
let missing = crate::materialize_parsed_token2022_stateful_snapshot(
account_address.as_str(),
44,
&state,
);
assert!(missing.is_err());
let wrong_context = crate::Token2022StatefulContext {
expected_group_address: std::option::Option::Some(account_key(14)),
};
let wrong = crate::materialize_parsed_token2022_stateful_snapshot_with_context(
account_address.as_str(),
44,
&state,
&wrong_context,
);
assert!(wrong.is_err());
let context = crate::Token2022StatefulContext {
expected_group_address: std::option::Option::Some(group_address),
};
let valid = crate::materialize_parsed_token2022_stateful_snapshot_with_context(
account_address.as_str(),
44,
&state,
&context,
);
assert_eq!(
valid.as_ref().map(|bundle| return bundle.outputs.len()),
std::result::Result::Ok(2)
);
}
#[test]
fn mint_and_account_fee_extensions_route_to_the_fee_owner_once() {
let mint_address = account_key(15);
let mint = kb_lib::DcToken2022State {
kind: kb_lib::DcToken2022StateKind::Mint,
base_fields: serde_json::json!({"supply":"1","decimals":0,"initialized":true}),
base_hex: "00".repeat(82),
account_type: std::option::Option::Some(1),
extensions: std::vec![kb_lib::DcToken2022TlvEntry {
extension_type: 1,
extension_name: "transfer_fee_config",
value_hex: "01".to_string(),
value_fields: serde_json::json!({"withheldAmount":"7"}),
}],
};
let mint_bundle =
crate::materialize_parsed_token2022_stateful_snapshot(mint_address.as_str(), 50, &mint);
assert_eq!(
mint_bundle.as_ref().map(|bundle| return bundle.outputs.len()),
std::result::Result::Ok(2)
);
assert_eq!(
mint_bundle
.as_ref()
.map(|bundle| return bundle.outputs[1].payload_json["provenance"]["processorName"]
.clone()),
std::result::Result::Ok(serde_json::json!("fees"))
);
let account_address = account_key(16);
let account = kb_lib::DcToken2022State {
kind: kb_lib::DcToken2022StateKind::Account,
base_fields: serde_json::json!({"amount":"0","state":"initialized"}),
base_hex: "00".repeat(165),
account_type: std::option::Option::Some(2),
extensions: std::vec![kb_lib::DcToken2022TlvEntry {
extension_type: 17,
extension_name: "confidential_transfer_fee_amount",
value_hex: "02".to_string(),
value_fields: serde_json::json!({"withheldAmount":"ciphertext"}),
}],
};
let account_bundle = crate::materialize_parsed_token2022_stateful_snapshot(
account_address.as_str(),
51,
&account,
);
assert_eq!(
account_bundle.as_ref().map(|bundle| return bundle.outputs.len()),
std::result::Result::Ok(2)
);
assert_eq!(
account_bundle
.as_ref()
.map(|bundle| return bundle.outputs[1].payload_json["confidentialValuesDecrypted"]
.clone()),
std::result::Result::Ok(serde_json::json!(false))
);
}
#[test]
fn mint_admin_extensions_route_to_the_admin_owner_once() {
let account_key = account_key(12);
let mut state = mint_state(account_key.as_str());
state.extensions.push(kb_lib::DcToken2022TlvEntry {
extension_type: 6,
extension_name: "default_account_state",
value_hex: "02".to_string(),
value_fields: serde_json::json!({"state": 2}),
});
let bundle =
crate::materialize_parsed_token2022_stateful_snapshot(account_key.as_str(), 17, &state);
assert_eq!(
bundle.as_ref().map(|value| return value.outputs.len()),
std::result::Result::Ok(4)
);
assert_eq!(
bundle.as_ref().map(|value| {
return value
.outputs
.iter()
.filter(|output| {
return output.payload_json["domain"]
== serde_json::json!("token2022_extension_admin_state");
})
.count();
}),
std::result::Result::Ok(1)
);
}
#[test]
fn complete_rpc_account_routes_only_after_owner_size_and_context_validation() {
let account = account_key(21);
let request = crate::Token2022StatefulReadRequest {
query_role: "execution".to_string(),
account: kb_lib::MdPubkey(account.clone()),
kind: kb_lib::DcToken2022StateKind::Mint,
min_context_slot: std::option::Option::Some(40),
max_data_bytes: 82,
context: crate::Token2022StatefulContext::default(),
};
let result = kb_onchain_transport::AccountInfoResult {
context: kb_onchain_transport::RpcResponseContext {
slot: 41,
api_version: std::option::Option::None,
},
account: std::option::Option::Some(kb_onchain_transport::AccountInfoValue {
lamports: 1,
owner: kb_lib::MdProgramId(kb_program_ids::SPL_TOKEN2022_PROGRAM_ID.to_string()),
executable: false,
rent_epoch: 0,
space: 82,
data: std::vec![0; 82],
}),
};
let materialized = crate::materialize_token2022_account_info_result(&request, &result);
assert_eq!(
materialized.as_ref().map(|value| return value.context_slot),
std::result::Result::Ok(41)
);
assert_eq!(
materialized.as_ref().map(|value| return value.snapshot.outputs.len()),
std::result::Result::Ok(1)
);
}
#[test]
fn incomplete_foreign_executable_and_stale_rpc_accounts_fail_closed() {
let account = account_key(22);
let request = crate::Token2022StatefulReadRequest {
query_role: "execution".to_string(),
account: kb_lib::MdPubkey(account),
kind: kb_lib::DcToken2022StateKind::Mint,
min_context_slot: std::option::Option::Some(50),
max_data_bytes: 82,
context: crate::Token2022StatefulContext::default(),
};
let base = kb_onchain_transport::AccountInfoValue {
lamports: 1,
owner: kb_lib::MdProgramId(kb_program_ids::SPL_TOKEN2022_PROGRAM_ID.to_string()),
executable: false,
rent_epoch: 0,
space: 82,
data: std::vec![0; 82],
};
let stale = kb_onchain_transport::AccountInfoResult {
context: kb_onchain_transport::RpcResponseContext {
slot: 49,
api_version: std::option::Option::None,
},
account: std::option::Option::Some(base.clone()),
};
assert!(crate::materialize_token2022_account_info_result(&request, &stale).is_err());
let mut foreign = base.clone();
foreign.owner = kb_lib::MdProgramId(kb_program_ids::SPL_TOKEN_PROGRAM_ID.to_string());
let foreign = kb_onchain_transport::AccountInfoResult {
context: kb_onchain_transport::RpcResponseContext {
slot: 50,
api_version: std::option::Option::None,
},
account: std::option::Option::Some(foreign),
};
assert!(crate::materialize_token2022_account_info_result(&request, &foreign).is_err());
let mut executable = base.clone();
executable.executable = true;
let executable = kb_onchain_transport::AccountInfoResult {
context: kb_onchain_transport::RpcResponseContext {
slot: 50,
api_version: std::option::Option::None,
},
account: std::option::Option::Some(executable),
};
assert!(crate::materialize_token2022_account_info_result(&request, &executable).is_err());
let mut incomplete = base;
incomplete.data.pop();
let incomplete = kb_onchain_transport::AccountInfoResult {
context: kb_onchain_transport::RpcResponseContext {
slot: 50,
api_version: std::option::Option::None,
},
account: std::option::Option::Some(incomplete),
};
assert!(crate::materialize_token2022_account_info_result(&request, &incomplete).is_err());
}
}