0.5.1-pre.002

This commit is contained in:
2026-08-09 19:34:08 +02:00
parent 816eee59a9
commit 6a680767ae
767 changed files with 12257 additions and 12195 deletions

113
ks-pipeline/CHANGELOG.md Normal file
View File

@@ -0,0 +1,113 @@
<!-- file: ks-pipeline/CHANGELOG.md -->
<!-- version: 31 -->
# CHANGELOG — ks-pipeline
## `0.5.1-pre.002`
- renomme `kb-pipeline` en `ks-pipeline` et `kb_pipeline` en `ks_pipeline` ;
- aligne son target racine de tracing et les descripteurs de crate directement liés au package ;
- conserve les identités d'exécuteurs/décodeurs `kb-lib.*` jusqu'à `pre.003`.
## `0.4.8-pre.015`
- `pre.015-delta-fix-001` retire du TODO les validations Solana Program Metadata déjà closes par le replay mainnet `pre.008`, la campagne Devnet `pre.010` et le desktop `pre.014`, ainsi que la dette Metaplex Print/Burn/collection/pNFT fermée en `pre.013` ;
- le delta initial ajoute un test dAPI externe dédié à Token-2022 Token Metadata afin de vérifier les exports crate-root des lectures stateful, de la validation de plage `Emit`, de linspection du `returnData` et de la postcondition dautorité ;
- ne modifie aucun contrat dexécution ou de stockage.
## `0.4.8-pre.013`
- `pre.013-delta-fix-031` expose `MetaplexTokenMetadataAccountKind::TokenOwnedEscrow` dans les lectures stateful et réutilise le décodeur strict déjà présent dans `ks-lib`, y compris validation du propriétaire, du discriminant, du PDA dautorité et du bump ;
- ajoute une régression de projection canonique pour un escrow `TokenOwner` et conserve `base_token`, `authority_kind`, `creator` et `stored_bump` comme postconditions queryables.
- `pre.013-delta-fix-030` conserve les simulations Metaplex exactes échouées comme résultats observables lorsque `submit=false`, afin que les probes de disponibilité puissent classifier un refus runtime sans le transformer en erreur d'orchestration ;
- maintient une barrière stricte pour toute soumission : `submit=true` exige toujours `simulation_succeeded=true`, et une simulation négative ne peut jamais produire `send_authorized=true` ;
- ajoute une régression dédiée au contrat « failed simulation reportable for probe only ».
- `pre.013-delta-fix-021` ajoute une régression stateful construisant un compte `Edition` compact de 42 octets et exige sa matérialisation autoritative, afin de couvrir directement lallocation produite par un `Print` courant ;
- enrichit `metaplex_stateful_decode_failed` avec ladresse et le `kind` de la requête, sans affaiblir le refus des comptes invalides.
- `pre.013-delta-fix-020` réembarque explicitement le variant public `MetaplexTokenMetadataAccountKind::EditionMarker { mint, edition }` afin quun overlay incomplet de `fix-019` ne puisse plus laisser le scénario `Print -> Burn` compiler contre un contrat stateful ancien ;
- ajoute un test dAPI externe qui construit et sérialise une requête `EditionMarker` uniquement via les exports crate-root ;
- valide également les pubkeys de dérivation `mint` et `token` directement dans `MetaplexTokenMetadataStatefulReadRequest::validate`.
- `pre.013-delta-fix-019` ajoute `MetaplexTokenMetadataAccountKind::EditionMarker { mint, edition }` aux lectures stateful afin de décoder et matérialiser explicitement `EditionMarker`/`EditionMarkerV2` lors des campagnes déditions imprimées ;
- la projection stateful conserve le groupe, lindex de bit, le masque, `edition_taken`, le ledger borné et le payload canonique sans inventer de sémantique de fermeture après burn.
## `0.4.8-pre.013` — borne stateful Metaplex
- aligne `MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES` sur `ks_onchain_transport::MAX_COMPLETE_ACCOUNT_DATA_BYTES`, soit `65536` octets pour une lecture complète `getAccountInfo` ;
- valide la borne dans `MetaplexTokenMetadataStatefulReadRequest` avant la construction de la requête RPC ;
- conserve séparées les bornes de décodage offline et la borne du transport d'exécution ;
- ajoute une régression empêchant de réintroduire une limite stateful supérieure au contrat du transport.
## `0.4.8-pre.011` — validation Token-2022 Token Metadata
- ajoute lextraction bornée du retour base64 produit par `Emit` ;
- aligne la fixture de simulation Token-2022 sur le variant canonique `ExApiExecutionBlockhashKind::Latest` ;
- refuse les plages `Emit` ouvertes avec `start` sans `end`, en cohérence avec le builder et la limite de retour Solana ;
- décode uniquement un retour complet et conserve un retour partiel sans inventer un état ;
- ajoute la postcondition stateful de changement ou suppression de lautorité metadata.
## 0.4.8-pre.010
- remplace lassertion runtime sur la relation entre borne RPC et borne de décodage par une assertion `const`, afin de supprimer le dernier warning `clippy::assertions_on_constants` ;
- borne les lectures complètes Solana Program Metadata au maximum réellement accepté par ladaptateur RPC, soit `65536` octets ;
- conserve séparément la capacité du décodeur à valider des données plus grandes provenant de sources offline ou dun futur lecteur par tranches ;
- ajoute un test empêchant de réaligner par erreur la borne stateful sur la limite de décodage de `10 MiB` ;
## 0.4.8-pre.008 — orchestration Solana Program Metadata généraliste
- ajoute les lectures de comptes `Buffer` et `Metadata` bornées, avec décodage et projection autoritative délégués à `ks-lib` ;
- ajoute le préflight stateful des neuf opérations stables, les preuves de préfinancement rent et lévaluation commune par `ExSafetyChecker` ;
- ajoute la readiness simulation-first, la résolution des signers et les postconditions confirmées sans transformer une absence de lecture en succès ;
- publie la matrice `METADATA_SOLANA_PROGRAM_PIPELINE_MATRIX.json` et maintient une frontière stricte avec les fixtures et scénarios Devnet de `ks-pipeline-demo-scenarios` ;
- renomme les modules internes selon la hiérarchie de domaine : `metadata_*` pour Metaplex et Solana Program Metadata, `spl_*` pour ATA, ElGamal Registry, Token et Token-2022, tout en conservant les fonctions publiques existantes ;
- remplace les assertions runtime sur constantes du test dAPI externe par des assertions `const`, afin de rester propre sous Clippy.
## 0.4.7-pre.016 — clôture documentaire Metaplex
- aligne la documentation du pipeline sur la qualification finale des preuves réseau ;
- reporte les futures postconditions spécialisées sans introduire de branche Metaplex dans lorchestration généraliste.
## 0.4.7-pre.011 — réouverture des validations réseau
### Documentation
- réorganisation des quatre documents de crate ;
- clarification que les campagnes Devnet appartiennent à `ks-pipeline-demo-scenarios`, tandis que `ks-pipeline` reste généraliste et indépendant des scénarios.
## 0.4.7-pre.007 — orchestration Metaplex généraliste
- ajout des lectures stateful bornées, du préflight, de lorchestration simulation-first et des postconditions ;
- correction de la rustdoc et ajout dexemples publics ;
- ajout des diagnostics de dépréciation et de lapprobation opérateur.
## 0.4.6
- alignement de la crate sur la version fonctionnelle bot3 `0.4.6` ;
- clôture des tâches de migration applicables et report explicite des évolutions ultérieures dans le TODO.
## 0.1.0-pre.074
- suppression du blocant daudit générique après production de `V0_4_6_ALIGNMENT_AUDIT.md`, désormais archivé sous `olddocs/archivekbot3/`, aucun écart pipeline concret supplémentaire nayant été démontré ;
## 0.1.0-pre.073
- ajout du guide transversal [`docs/guides/REPLAY_CORE_EXTRACTION_AND_MATERIALIZATION.md`](../docs/guides/REPLAY_CORE_EXTRACTION_AND_MATERIALIZATION.md) ;
## 0.1.0-pre.072
- reclassement du TODO selon les blocants avant `0.4.6`, les travaux `0.4.7`, les versions ultérieures et les dépendances conditionnelles.
## 0.1.0-pre.070
- enrichissement du `USAGE.md` avec plusieurs exemples couvrant les principales familles dAPI publiques ;
- ajout du contrat documentaire de la crate ;
- classement des APIs publiques par campagnes, replay et inspections stateful ;
- clarification des invariants de déterminisme, bornage et matérialisation ;
- inscription des travaux Metaplex et ElGamal restants dans le TODO.
## 0.1.0-pre.062
- migration du pipeline bot2 dans larchitecture consolidée bot3 ;
- reprise du backfill, de lextraction Core, du decode replay et des matérialisations ;
- migration des préflights, corrélations et orchestrations Token-2022 ;
- migration des contrats stateful SPL Token, ATA et registre ElGamal ;
- adaptation aux normes Rust 2024 et Khadhroony bot3.

35
ks-pipeline/Cargo.toml Normal file
View File

@@ -0,0 +1,35 @@
# file: ks-pipeline/Cargo.toml
# version: 16
[package]
name = "ks-pipeline"
version.workspace = true
edition.workspace = true
license.workspace = true
publish.workspace = true
[dependencies]
async-trait.workspace = true
base64.workspace = true
bs58.workspace = true
chrono.workspace = true
futures-util.workspace = true
ks-core = { path = "../ks-core" }
ks-config = { path = "../ks-config" }
ks-lib = { path = "../ks-lib" }
ks-onchain-transport = { path = "../ks-onchain-transport" }
ks-program-ids = { path = "../ks-program-ids" }
ks-store = { path = "../ks-store" }
serde.workspace = true
serde_json.workspace = true
solana-address-lookup-table-interface.workspace = true
solana-pubkey.workspace = true
spl-elgamal-registry-interface.workspace = true
spl-associated-token-account-interface.workspace = true
sha2.workspace = true
tokio.workspace = true
tracing.workspace = true
uuid.workspace = true
[lints]
workspace = true

54
ks-pipeline/README.md Normal file
View File

@@ -0,0 +1,54 @@
<!-- file: ks-pipeline/README.md -->
<!-- version: 10 -->
# ks-pipeline
`ks-pipeline` est le pipeline généraliste de `khadhroony-bot3`. Il coordonne les transports, le stockage et `ks-lib` sans dépendre dun scénario de démonstration ni dun cluster particulier.
## Responsabilités
- backfill HTTP borné ;
- insertion canonique ;
- extraction Core ;
- replay contextualisé ;
- matérialisation optionnelle et idempotente ;
- lectures stateful et corrélations ;
- préflights dexécution ;
- résolution des signers ;
- orchestration simulation-first, contrôles de readiness et postvalidation ;
- diagnostics stables exploitables par des applications ou scénarios.
Pour Metaplex Token Metadata, la crate expose les lectures stateful bornées, le préflight, la validation de readiness et lagrégation des postconditions. Une simulation exacte négative peut être conservée comme résultat observable uniquement lorsque `submit=false`; toute soumission continue dexiger une simulation réussie et ne peut jamais être autorisée à partir dun probe négatif.
Pour Solana Program Metadata, elle expose les mêmes frontières réutilisables pour les neuf opérations stables : lecture `Buffer`/`Metadata`, préflight stateful, gouvernance `ExSafetyChecker`, readiness simulation-first et postconditions. Elle ne contient ni fixture Devnet, ni wallet de démonstration, ni séquence métier spécifique à un test.
## Surface publique principale
- campagnes de backfill et progression ;
- extraction Core et decode replay ;
- inspections stateful Solana Core et SPL ;
- `read_metaplex_token_metadata_stateful_snapshot` ;
- `inspect_metaplex_token_metadata_preflight` ;
- `validate_metaplex_token_metadata_execution_readiness` ;
- `summarize_metaplex_token_metadata_postconditions` ;
- `read_solana_program_metadata_stateful_snapshot` ;
- `inspect_solana_program_metadata_preflight` ;
- `validate_solana_program_metadata_execution_readiness` ;
- `inspect_solana_program_metadata_post_execution`.
Les fichiers internes suivent la même nomenclature que `ks-lib` : préfixe `metadata_` pour les programmes metadata, préfixe `spl_` pour les programmes SPL et préfixe `solana_` uniquement pour les contrats Solana Core transversaux. Les noms des fonctions publiques sont conservés pour éviter une rupture dAPI.
Voir [USAGE.md](USAGE.md) pour les exemples et invariants.
## Relations avec le workspace
La crate coordonne `ks-onchain-transport`, `ks-store`, `ks-lib`, `ks-core` et `ks-program-ids`. Elle ne dépend jamais de `ks-pipeline-demo-scenarios` ni de `kb-app-demo-desktop`. Les fixtures, wallets temporaires et appels RPC de démonstration appartiennent aux scénarios ou aux applications qui consomment ces contrats.
## Documentation
- [Guide dutilisation](USAGE.md)
- [Travaux restant à réaliser](TODO.md)
- [Historique des changements](CHANGELOG.md)
- [Architecture du pipeline](../docs/architecture/PIPELINE_ARCHITECTURE.md)
- validation bornée du retour `Emit` Token-2022 et postcondition stateful de lautorité metadata ;

19
ks-pipeline/TODO.md Normal file
View File

@@ -0,0 +1,19 @@
<!-- file: ks-pipeline/TODO.md -->
<!-- version: 14 -->
# TODO — ks-pipeline
## Réaudit ultérieur des surfaces historiques
- [ ] Version à déterminer - après le réaudit des exécuteurs Solana Core et SPL dans `ks-lib`, vérifier que le pipeline orchestre sans filtrage implicite les opérations courantes et les opérations obsolètes explicitement approuvées.
- [ ] Préflight et diagnostics - propager les remplacements canoniques, avertissements de dépréciation et approbations opérateur des surfaces réauditées.
- [ ] Tests - ajouter les cas pipeline nécessaires aux opérations dépréciées réintroduites, sans créer de chemin pour les versions remplacées intermédiaires.
## Versions ultérieures
- [ ] Dette technique - auditer les duplications résiduelles entre les orchestrations SPL Token classique et Token-2022.
## Report conditionnel — registre ElGamal
- [ ] Réseau - confirmer lexistence et le déploiement du programme avant toute campagne réelle.
- [ ] Intégration - compléter uniquement les couches justifiées par un scénario réellement exécutable.

366
ks-pipeline/USAGE.md Normal file
View File

@@ -0,0 +1,366 @@
<!-- file: ks-pipeline/USAGE.md -->
<!-- version: 17 -->
# Utilisation de ks-pipeline
## Lecture stateful des Edition Markers Metaplex
Les lectures stateful `Edition` acceptent les allocations Metaplex courantes après validation par `ks-lib`. Un `Print` récent produit notamment une allocation compacte de 42 octets alors que la structure Borsh `Edition` sérialise 41 octets ; le pipeline ne recalcule pas cette règle et consomme uniquement le snapshot déjà validé par le décodeur. Les erreurs de décodage stateful conservent ladresse et le `kind` demandés pour localiser la postcondition fautive.
Les campagnes déditions imprimées peuvent demander un snapshot borné `EditionMarker` en fournissant le mint maître et le numéro dédition. Le pipeline dérive et vérifie le contrat du compte via le décodeur Metaplex existant, puis expose notamment `marker_group`, `edition`, `byte_index`, `bit_mask`, `edition_taken` et le ledger. Cette lecture sert à prouver quun numéro dédition a été consommé ; elle ne suppose pas que le marker est fermé lors dun burn. La validation de requête contrôle aussi les pubkeys utilisées pour les dérivations `Edition`, `EditionMarker` et `TokenRecord` avant tout accès RPC.
## Objectif
La crate expose les campagnes bornées dacquisition, dextraction, de replay et les inspections stateful nécessaires aux applications et scénarios.
## Valider une requête dextraction Core
```rust
fn validate_pending_extraction() -> ks_core::Result<()> {
let request = ks_pipeline::CoreExtractionRequest {
source: ks_pipeline::CoreExtractionSource::Pending,
limit: 1_000,
max_concurrent_extractions: 4,
force_replay: false,
};
request.validate()
}
```
Une extraction ciblée peut utiliser `CoreExtractionSource::Signatures`, `SlotRange` ou `ProgramId`.
## Exécuter une campagne dextraction Core
```rust
async fn run_core_extraction<S, O>(
store: &S,
observer: &O,
request: ks_pipeline::CoreExtractionRequest,
) -> ks_core::Result<ks_pipeline::CoreExtractionSummary>
where
S: ks_store::CanonicalTransactionStore
+ ks_store::CoreExtractionStore
+ Sync,
O: ks_pipeline::CoreExtractionObserver,
{
let result = ks_pipeline::execute_core_extraction(store, observer, request).await;
match result {
Ok(summary) => Ok(summary),
Err(error) => Err(error),
}
}
```
## Préparer une requête de decode replay
```rust
fn validate_decode_request(
selection: ks_store::DecodeSelectionFilter,
) -> ks_core::Result<()> {
let request = ks_pipeline::DecodeReplayRequest {
campaign_id: "manual-replay-001".to_string(),
selection,
decoder_names: Vec::new(),
dispatch_policy: ks_pipeline::DecodeDispatchPolicy::HighestPriority,
max_concurrent_inputs: 4,
force_replay: false,
force_replay_all_matching: false,
materialize_after_decode: true,
};
request.validate()
}
```
Une liste vide dans `decoder_names` signifie que tous les décodeurs fournis à lorchestrateur restent éligibles.
## Exécuter un decode replay
```rust
async fn run_decode_replay<S, O>(
store: &S,
observer: &O,
request: ks_pipeline::DecodeReplayRequest,
decoders: &[&dyn ks_lib::MdApiInstructionDecoder],
materializers: &[&dyn ks_lib::MdApiEventMaterializer],
) -> ks_core::Result<ks_pipeline::DecodeReplaySummary>
where
S: ks_store::DecodeReplayStore + Sync,
O: ks_pipeline::DecodeReplayObserver,
{
let result = ks_pipeline::execute_decode_replay(
store,
observer,
request,
decoders,
materializers,
)
.await;
match result {
Ok(summary) => Ok(summary),
Err(error) => Err(error),
}
}
```
## Backfill HTTP
La surface principale utilise `BackfillRequest`, `BackfillObserver`, `execute_http_backfill` et `BackfillSummary`.
```rust
async fn run_backfill<S, O>(
store: &S,
observer: &O,
request: ks_pipeline::BackfillRequest,
) -> ks_core::Result<ks_pipeline::BackfillSummary>
where
S: ks_store::CanonicalTransactionStore + Sync,
O: ks_pipeline::BackfillObserver,
{
let result = ks_pipeline::execute_http_backfill(store, observer, request).await;
match result {
Ok(summary) => Ok(summary),
Err(error) => Err(error),
}
}
```
## Inspections stateful
Les familles publiques comprennent :
- `inspect_solana_core_stateful_readiness` ;
- inspections SPL Token et ATA ;
- inspections et corrélations Token-2022 ;
- préflight cryptographique Token-2022 ;
- lecture et matérialisation stateful du registre ElGamal.
```rust
async fn inspect_classic_token<S>(
store: &S,
request: ks_pipeline::SplTokenStatefulReadinessRequest,
) -> ks_core::Result<ks_pipeline::SplTokenStatefulReadinessReport>
where
S: ks_store::CanonicalTransactionStore + Sync,
{
let result = ks_pipeline::inspect_spl_token_stateful_readiness(store, request).await;
match result {
Ok(report) => Ok(report),
Err(error) => Err(error),
}
}
```
Les rapports stateful ne constituent jamais une autorisation implicite dexécution.
## Nomenclature interne des modules
Les modules privés sont organisés par domaine :
- `metadata_metaplex_token_metadata_*` ;
- `metadata_solana_program_*` ;
- `spl_ata_stateful` et `spl_elgamal_registry_stateful` ;
- `spl_token_stateful` et `spl_token_2022_*` ;
- `solana_stateful` uniquement pour les primitives Solana Core partagées.
Cette réorganisation ne renomme pas les fonctions et structures publiques existantes.
## Valider historiquement Solana Program Metadata
La validation historique reste un enchaînement de surfaces généralistes, pas un scénario Devnet :
1. `demo_backfill` hydrate un échantillon borné de signatures confirmées ;
2. lextraction Core résout les instructions et comptes ;
3. `demo_decode_replay` sélectionne le Program ID `ProgM6JCCvbYkfKqJYHePx4xxSUSqJp7rh8Lyv7nk7S` et le décodeur `metadata.solana_program_metadata` ;
4. `materializeAfterDecode` active `materializer.metadata.solana_program_metadata` ;
5. la requête bornée des événements matérialisés permet de vérifier les faits produits.
Le registre générique du desktop fournit désormais ce décodeur et ce matérialiseur. `ks-pipeline-demo-scenarios` nintervient pas dans ce parcours historique.
## Lire et prévalider Solana Program Metadata
```rust
async fn read_program_metadata(
pool: &ks_onchain_transport::HttpEndpointPool,
account: ks_lib::MdPubkey,
) -> ks_core::Result<ks_pipeline::SolanaProgramMetadataStatefulReadResult> {
let request = ks_pipeline::SolanaProgramMetadataStatefulReadRequest {
query_role: "rpc".to_string(),
account,
expected_state: ks_pipeline::SolanaProgramMetadataExpectedAccountState::Any,
min_context_slot: std::option::Option::None,
max_data_bytes: ks_pipeline::MAX_SOLANA_PROGRAM_METADATA_STATEFUL_ACCOUNT_BYTES,
};
let result = ks_pipeline::read_solana_program_metadata_stateful_snapshot(pool, &request).await;
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
```
`MAX_SOLANA_PROGRAM_METADATA_STATEFUL_ACCOUNT_BYTES` représente la borne dune lecture RPC complète et vaut actuellement `65536`. La capacité de décodage `DC_METADATA_SPM_MAX_ACCOUNT_BYTES` reste distincte pour les données déjà disponibles offline ou pour un futur lecteur par tranches.
Le préflight reçoit lintent typé, le plan produit par `ExMetadataSolanaProgramMetadataExecutor`, les snapshots confirmés et, pour les opérations qui allouent ou agrandissent un compte, une observation explicite du minimum de rent. Il refuse un plan `Deny`, mais les opérations dangereuses restent exécutables lorsquelles sont explicitement approuvées et que lenveloppe commune de sécurité est satisfaite.
Après simulation du message exact, `validate_solana_program_metadata_execution_readiness` vérifie les signers et lautorisation de soumission. Après confirmation, `inspect_solana_program_metadata_post_execution` compare les lectures avant/après pour les neuf opérations.
## Observateurs
Les campagnes longues exposent des traits dobservation distincts pour le backfill, lextraction Core et le replay. Lobservateur peut publier la progression et participer à lannulation coopérative selon le contrat concerné.
## Erreurs et invariants
- toutes les campagnes sont bornées ;
- la progression persistée ne doit avancer quaprès traitement cohérent ;
- le replay doit rester déterministe pour une même entrée et une même version de pipeline ;
- une matérialisation ne doit pas inventer un état confirmé ;
- les erreurs de transport, stockage, décodage et préflight restent distinguées.
## Tests de référence
- tests de frontière contiguë et reprise du backfill ;
- tests dextraction Core et didempotence ;
- tests de decode replay, dispatch et matérialisation ;
- tests stateful SPL Token, ATA et Token-2022 ;
- tests de preuves, préflight cryptographique et postconditions ;
- tests du registre ElGamal fail-closed.
## Limites durables
- la crate orchestre les traitements mais ne fournit pas dinterface opérateur ;
- elle ne conserve pas de secret de wallet ;
- elle ne remplace pas les scénarios Devnet et validations explicites de `ks-pipeline-demo-scenarios`.
## Lire un compte Metaplex Token Metadata
La lecture stateful exige un endpoint HTTP, une catégorie de compte explicite et une borne stricte sur les données décodées.
```rust
async fn read_metaplex_metadata(
pool: &ks_onchain_transport::HttpEndpointPool,
metadata: ks_lib::MdPubkey,
) -> ks_core::Result<ks_pipeline::MetaplexTokenMetadataStatefulReadResult> {
let request = ks_pipeline::MetaplexTokenMetadataStatefulReadRequest {
query_role: "rpc".to_string(),
account: metadata,
kind: ks_pipeline::MetaplexTokenMetadataAccountKind::Metadata,
min_context_slot: std::option::Option::None,
max_data_bytes: ks_pipeline::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES,
};
let result = ks_pipeline::read_metaplex_token_metadata_stateful_snapshot(
pool,
&request,
)
.await;
match result {
Ok(snapshot) => Ok(snapshot),
Err(error) => Err(error),
}
}
```
Pour une edition, fournir le mint dans `MetaplexTokenMetadataAccountKind::Edition`. Pour un token record programmable, fournir le mint et le token account dans `TokenRecord`. Pour un Token Owned Escrow, utiliser `TokenOwnedEscrow` : le décodeur récupère le mint parent et lautorité depuis le compte, puis valide lui-même le PDA et le bump avant projection stateful.
`MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES` représente la borne d'une lecture RPC complète et suit `ks_onchain_transport::MAX_COMPLETE_ACCOUNT_DATA_BYTES`, soit actuellement `65536` octets. Cette borne stateful ne doit pas être confondue avec les bornes propres aux décodeurs pour des octets déjà disponibles offline ou avec un futur lecteur par tranches.
## Inspecter le préflight Metaplex
Le préflight lie un plan préparé par `ks-lib` à des snapshots confirmés et applique la politique des opérations dépréciées.
```rust
fn inspect_metaplex_preflight(
plan: ks_lib::ExApiPreparedExecutionPlan,
snapshots: Vec<ks_pipeline::MetaplexTokenMetadataStatefulReadResult>,
allow_deprecated_operation: bool,
) -> ks_core::Result<ks_pipeline::MetaplexTokenMetadataPreflightReport> {
let request = ks_pipeline::MetaplexTokenMetadataPreflightRequest {
plan,
snapshots,
allow_deprecated_operation,
};
ks_pipeline::inspect_metaplex_token_metadata_preflight(&request)
}
```
Une opération marquée dépréciée par `ks-lib` est refusée lorsque `allow_deprecated_operation` vaut `false`.
### Exigences stateful par famille
Le pipeline ne doit pas considérer un intent sérialisé comme prêt à exécuter sans snapshots adaptés :
- création : lire le mint et vérifier owner, décimales, supply, mint authority et freeze authority avant la construction finale ; les PDA metadata et edition doivent être absents ou compatibles selon la variante ;
- mutation : lire metadata et, lorsque requis, edition, token account, token record, delegate record, collection metadata et rule set ;
- vérification : prouver la relation dautorité visée avant linstruction et relire le bit ou lenregistrement modifié après confirmation ;
- délégation, verrouillage, transfert et burn : corréler le mint, le token account, le token record programmable et les autorités exactes ;
- escrow, print, use, collect, migrate et resize : fournir les comptes spécialisés de la variante et définir une postcondition observable.
Une campagne sans modèle préparé doit rester bloquée. Elle ne doit jamais réutiliser lintent de lopération précédemment sélectionnée.
## Valider lenveloppe dexécution Metaplex
Lorchestration refuse la signature ou la soumission tant que la simulation exacte du message, les signers et la confirmation opérateur ne sont pas cohérents.
```rust
fn validate_metaplex_execution(
plan: ks_lib::ExApiPreparedExecutionPlan,
preflight: ks_pipeline::MetaplexTokenMetadataPreflightReport,
message_hash: String,
resolved_signers: Vec<ks_lib::MdPubkey>,
) -> ks_core::Result<ks_pipeline::MetaplexTokenMetadataExecutionReadinessReport> {
let request = ks_pipeline::MetaplexTokenMetadataExecutionReadinessRequest {
plan,
preflight,
message_hash: message_hash.clone(),
simulated_message_hash: message_hash,
simulated: true,
simulation_succeeded: true,
resolved_signers,
submit: false,
operator_confirmed: false,
};
ks_pipeline::validate_metaplex_token_metadata_execution_readiness(&request)
}
```
Pour une soumission réelle, `submit` et `operator_confirmed` doivent être vrais et le plan ne doit plus être en `dry_run`.
Une requête `submit=false` peut conserver une simulation exacte ayant échoué afin quun appelant classe explicitement une indisponibilité runtime. Dans ce cas, le rapport reste non soumettable et contient le marqueur `failed_simulation_retained_for_probe_only`. Avec `submit=true`, une simulation échouée reste une erreur de readiness et `send_authorized` ne peut jamais devenir vrai.
## Agréger les postconditions Metaplex
```rust
fn summarize_metaplex_postconditions(
statuses: &[ks_pipeline::MetaplexTokenMetadataPostconditionStatus],
) -> ks_pipeline::MetaplexTokenMetadataPostconditionStatus {
ks_pipeline::summarize_metaplex_token_metadata_postconditions(statuses)
}
```
`Contradicted` est prioritaire sur `Confirmed`, et labsence de postcondition applicable reste `NotApplicable`.
## Orchestration des scénarios Metaplex
`ks-pipeline-demo-scenarios` expose des parcours déclaratifs plutôt que cent combinaisons forcées. Chaque parcours déclare :
- la famille dasset ;
- la fixture requise ;
- létat initial ;
- les opérations ordonnées ;
- létat terminal attendu ;
- les comptes et projections nécessaires aux postconditions.
Lorsque `materialize_after_confirmation` est activé, une soumission exige au moins une lecture de postcondition. Après confirmation, les snapshots bornés sont décodés et projetés. Lorsque loption est désactivée, le runner conserve la simulation, la soumission, la confirmation et les postconditions demandées, mais német pas les projections de matérialisation.
## Validation Token-2022 Token Metadata
`inspect_token_2022_metadata_emit_simulation` valide le `returnData` dune simulation `Emit`. `inspect_token_2022_metadata_authority_postcondition` compare lautorité attendue au snapshot TLV autoritatif après confirmation.

1984
ks-pipeline/src/backfill.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
// file: ks-pipeline/src/constants.rs
// version: 3
//! Pipeline constants.
/// Canonical tracing target for pipeline orchestration.
pub(crate) const TRACING_TARGET: &str = "ks-pipeline";

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

321
ks-pipeline/src/lib.rs Normal file
View File

@@ -0,0 +1,321 @@
// file: ks-pipeline/src/lib.rs
// version: 22
#![forbid(unsafe_code)]
#![deny(unreachable_pub)]
#![warn(missing_docs)]
//! Pipeline orchestration boundary.
mod backfill;
mod constants;
mod core_extraction;
mod decode_replay;
mod metadata_metaplex_token_metadata_execution_orchestration;
mod metadata_metaplex_token_metadata_preflight;
mod metadata_metaplex_token_metadata_stateful;
mod metadata_solana_program_execution_orchestration;
mod metadata_solana_program_preflight;
mod metadata_solana_program_stateful;
mod plan;
mod solana_stateful;
mod spl_ata_stateful;
mod spl_elgamal_registry_stateful;
mod spl_token_2022_correlation;
mod spl_token_2022_crypto_preflight;
mod spl_token_2022_execution_orchestration;
mod spl_token_2022_metadata;
mod spl_token_2022_preflight;
mod spl_token_2022_proof_orchestration;
mod spl_token_2022_stateful;
mod spl_token_stateful;
/// Address category used by one targeted backfill campaign.
pub use self::backfill::BackfillAddressKind;
/// Chronological direction relative to one anchor signature.
pub use self::backfill::BackfillDirection;
/// Observer notified during HTTP backfill campaigns.
pub use self::backfill::BackfillObserver;
/// Progress event emitted by HTTP backfill campaigns.
pub use self::backfill::BackfillProgressEvent;
/// Severity of one HTTP backfill progress event.
pub use self::backfill::BackfillProgressLevel;
/// Complete bounded HTTP backfill request.
pub use self::backfill::BackfillRequest;
/// Candidate source used by one HTTP backfill campaign.
pub use self::backfill::BackfillSource;
/// Summary returned by one HTTP backfill campaign.
pub use self::backfill::BackfillSummary;
/// Executes one bounded HTTP transaction backfill campaign.
pub use self::backfill::execute_http_backfill;
/// Stable core extraction processor name.
pub use self::core_extraction::CORE_EXTRACTION_PROCESSOR_NAME;
/// Stable core extraction processor version.
pub use self::core_extraction::CORE_EXTRACTION_PROCESSOR_VERSION;
/// Stable core extraction stage code.
pub use self::core_extraction::CORE_EXTRACTION_STAGE;
/// Observer notified during core extraction campaigns.
pub use self::core_extraction::CoreExtractionObserver;
/// Progress event emitted by core extraction campaigns.
pub use self::core_extraction::CoreExtractionProgressEvent;
/// Severity of one core extraction progress event.
pub use self::core_extraction::CoreExtractionProgressLevel;
/// Bounded core extraction request.
pub use self::core_extraction::CoreExtractionRequest;
/// Source selection for a core extraction campaign.
pub use self::core_extraction::CoreExtractionSource;
/// Summary returned by a core extraction campaign.
pub use self::core_extraction::CoreExtractionSummary;
/// Executes one bounded core extraction campaign.
pub use self::core_extraction::execute_core_extraction;
/// Extracts one canonical raw transaction into normalized core rows.
pub use self::core_extraction::extract_raw_transaction_to_core;
/// Current common decode pipeline orchestration version.
pub use self::decode_replay::DECODE_PIPELINE_VERSION;
/// Deterministic decoder dispatch policy.
pub use self::decode_replay::DecodeDispatchPolicy;
/// Per-decoder terminal counters.
pub use self::decode_replay::DecodeProcessorSummary;
/// Decode replay observer contract.
pub use self::decode_replay::DecodeReplayObserver;
/// Decode replay progress event.
pub use self::decode_replay::DecodeReplayProgressEvent;
/// Decode replay progress severity.
pub use self::decode_replay::DecodeReplayProgressLevel;
/// Bounded contextual decode replay request.
pub use self::decode_replay::DecodeReplayRequest;
/// Final contextual decode replay summary.
pub use self::decode_replay::DecodeReplaySummary;
/// Stable materialization processing stage.
pub use self::decode_replay::EVENT_MATERIALIZATION_STAGE;
/// Stable instruction decode processing stage.
pub use self::decode_replay::INSTRUCTION_DECODE_STAGE;
/// Executes one contextual decode replay campaign.
pub use self::decode_replay::execute_decode_replay;
/// Creates one stable process-local decode campaign identifier.
pub use self::decode_replay::new_decode_campaign_id;
/// Maximum number of resolved signers accepted by one Metaplex execution envelope.
pub use self::metadata_metaplex_token_metadata_execution_orchestration::MAX_METAPLEX_TOKEN_METADATA_EXECUTION_SIGNERS;
/// Deterministic Metaplex execution-readiness report.
pub use self::metadata_metaplex_token_metadata_execution_orchestration::MetaplexTokenMetadataExecutionReadinessReport;
/// Complete Metaplex execution-readiness request.
pub use self::metadata_metaplex_token_metadata_execution_orchestration::MetaplexTokenMetadataExecutionReadinessRequest;
/// Result of one Metaplex stateful postcondition.
pub use self::metadata_metaplex_token_metadata_execution_orchestration::MetaplexTokenMetadataPostconditionStatus;
/// Aggregates Metaplex postconditions without inventing success.
pub use self::metadata_metaplex_token_metadata_execution_orchestration::summarize_metaplex_token_metadata_postconditions;
/// Validates the complete Metaplex execution envelope before signing.
pub use self::metadata_metaplex_token_metadata_execution_orchestration::validate_metaplex_token_metadata_execution_readiness;
/// Maximum number of correlated Metaplex snapshots accepted by one preflight.
pub use self::metadata_metaplex_token_metadata_preflight::MAX_METAPLEX_TOKEN_METADATA_PREFLIGHT_ACCOUNTS;
/// Deterministic Metaplex preflight report.
pub use self::metadata_metaplex_token_metadata_preflight::MetaplexTokenMetadataPreflightReport;
/// Complete Metaplex preflight request.
pub use self::metadata_metaplex_token_metadata_preflight::MetaplexTokenMetadataPreflightRequest;
/// Inspects one prepared Metaplex plan against confirmed state snapshots.
pub use self::metadata_metaplex_token_metadata_preflight::inspect_metaplex_token_metadata_preflight;
/// Maximum complete Metaplex account data accepted by one RPC read.
pub use self::metadata_metaplex_token_metadata_stateful::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES;
/// Supported Metaplex account category for one stateful read.
pub use self::metadata_metaplex_token_metadata_stateful::MetaplexTokenMetadataAccountKind;
/// One bounded Metaplex stateful read request.
pub use self::metadata_metaplex_token_metadata_stateful::MetaplexTokenMetadataStatefulReadRequest;
/// One bounded Metaplex stateful read result.
pub use self::metadata_metaplex_token_metadata_stateful::MetaplexTokenMetadataStatefulReadResult;
/// Canonical bounded Metaplex state snapshot.
pub use self::metadata_metaplex_token_metadata_stateful::MetaplexTokenMetadataStatefulSnapshot;
/// Validates one RPC account response and delegates parsing to `ks-lib`.
pub use self::metadata_metaplex_token_metadata_stateful::materialize_metaplex_token_metadata_account_info_result;
/// Reads one bounded Metaplex Token Metadata account.
pub use self::metadata_metaplex_token_metadata_stateful::read_metaplex_token_metadata_stateful_snapshot;
/// Maximum number of resolved signers accepted by one Program Metadata execution envelope.
pub use self::metadata_solana_program_execution_orchestration::MAX_SOLANA_PROGRAM_METADATA_EXECUTION_SIGNERS;
/// Complete Program Metadata execution-readiness report.
pub use self::metadata_solana_program_execution_orchestration::SolanaProgramMetadataExecutionReadinessReport;
/// Complete Program Metadata execution-readiness request.
pub use self::metadata_solana_program_execution_orchestration::SolanaProgramMetadataExecutionReadinessRequest;
/// Complete Program Metadata post-execution report.
pub use self::metadata_solana_program_execution_orchestration::SolanaProgramMetadataPostExecutionReport;
/// Complete Program Metadata post-execution request.
pub use self::metadata_solana_program_execution_orchestration::SolanaProgramMetadataPostExecutionRequest;
/// One Program Metadata postcondition.
pub use self::metadata_solana_program_execution_orchestration::SolanaProgramMetadataPostcondition;
/// Program Metadata postcondition status.
pub use self::metadata_solana_program_execution_orchestration::SolanaProgramMetadataPostconditionStatus;
/// Inspects confirmed Program Metadata post-execution state.
pub use self::metadata_solana_program_execution_orchestration::inspect_solana_program_metadata_post_execution;
/// Aggregates Program Metadata postconditions without invented success.
pub use self::metadata_solana_program_execution_orchestration::summarize_solana_program_metadata_postconditions;
/// Validates the complete Program Metadata execution envelope before signing.
pub use self::metadata_solana_program_execution_orchestration::validate_solana_program_metadata_execution_readiness;
/// Maximum number of bounded account snapshots accepted by one Program Metadata preflight.
pub use self::metadata_solana_program_preflight::MAX_SOLANA_PROGRAM_METADATA_PREFLIGHT_ACCOUNTS;
/// Deterministic Program Metadata preflight report.
pub use self::metadata_solana_program_preflight::SolanaProgramMetadataPreflightReport;
/// Complete Program Metadata preflight request.
pub use self::metadata_solana_program_preflight::SolanaProgramMetadataPreflightRequest;
/// One Program Metadata rent-exemption observation.
pub use self::metadata_solana_program_preflight::SolanaProgramMetadataRentObservation;
/// Inspects one Program Metadata plan against confirmed state.
pub use self::metadata_solana_program_preflight::inspect_solana_program_metadata_preflight;
/// Maximum complete Program Metadata account data accepted by one RPC read.
pub use self::metadata_solana_program_stateful::MAX_SOLANA_PROGRAM_METADATA_STATEFUL_ACCOUNT_BYTES;
/// Expected Program Metadata account state.
pub use self::metadata_solana_program_stateful::SolanaProgramMetadataExpectedAccountState;
/// Observed Program Metadata account state.
pub use self::metadata_solana_program_stateful::SolanaProgramMetadataObservedAccountState;
/// One bounded Program Metadata stateful read request.
pub use self::metadata_solana_program_stateful::SolanaProgramMetadataStatefulReadRequest;
/// One bounded Program Metadata stateful read result.
pub use self::metadata_solana_program_stateful::SolanaProgramMetadataStatefulReadResult;
/// Validates one Program Metadata RPC response and materializes its account state.
pub use self::metadata_solana_program_stateful::materialize_solana_program_metadata_account_info_result;
/// Reads one bounded Program Metadata account.
pub use self::metadata_solana_program_stateful::read_solana_program_metadata_stateful_snapshot;
/// Pipeline stage identifier.
pub use self::plan::PipelineStage;
/// Replay selection scope.
pub use self::plan::ReplayScope;
/// One machine-readable native Solana stateful readiness check.
pub use self::solana_stateful::SolanaCoreStatefulCheck;
/// One contextual fact measured during native Solana stateful readiness.
pub use self::solana_stateful::SolanaCoreStatefulFact;
/// Native Solana stateful readiness report.
pub use self::solana_stateful::SolanaCoreStatefulReadinessReport;
/// Native Solana stateful readiness request.
pub use self::solana_stateful::SolanaCoreStatefulReadinessRequest;
/// Native Solana stateful readiness status.
pub use self::solana_stateful::SolanaCoreStatefulReadinessStatus;
/// Inspects state required before simulating one native Solana operation.
pub use self::solana_stateful::inspect_solana_core_stateful_readiness;
/// Stateful invariants observed after one confirmed Associated Token Account execution.
pub use self::spl_ata_stateful::SplAssociatedTokenAccountPostExecutionReport;
/// One machine-readable Associated Token Account stateful check.
pub use self::spl_ata_stateful::SplAssociatedTokenAccountStatefulCheck;
/// One contextual Associated Token Account stateful fact.
pub use self::spl_ata_stateful::SplAssociatedTokenAccountStatefulFact;
/// Complete Associated Token Account stateful readiness report.
pub use self::spl_ata_stateful::SplAssociatedTokenAccountStatefulReadinessReport;
/// Complete request for one Associated Token Account stateful readiness inspection.
pub use self::spl_ata_stateful::SplAssociatedTokenAccountStatefulReadinessRequest;
/// Associated Token Account stateful readiness outcome.
pub use self::spl_ata_stateful::SplAssociatedTokenAccountStatefulReadinessStatus;
/// Verifies final Associated Token Account relationships after confirmed execution.
pub use self::spl_ata_stateful::inspect_spl_associated_token_account_post_execution;
/// Inspects state required before simulating one Associated Token Account operation.
pub use self::spl_ata_stateful::inspect_spl_associated_token_account_stateful_readiness;
/// Migrated ELGAMAL_REGISTRY_STATEFUL_ACCOUNT_BYTES contract.
pub use self::spl_elgamal_registry_stateful::ELGAMAL_REGISTRY_STATEFUL_ACCOUNT_BYTES;
/// Migrated ElGamalRegistryStatefulReadRequest contract.
pub use self::spl_elgamal_registry_stateful::ElGamalRegistryStatefulReadRequest;
/// Migrated ElGamalRegistryStatefulReadResult contract.
pub use self::spl_elgamal_registry_stateful::ElGamalRegistryStatefulReadResult;
/// Migrated ElGamalRegistryStatefulSnapshot contract.
pub use self::spl_elgamal_registry_stateful::ElGamalRegistryStatefulSnapshot;
/// Migrated materialize_elgamal_registry_account_info_result contract.
pub use self::spl_elgamal_registry_stateful::materialize_elgamal_registry_account_info_result;
/// Migrated materialize_elgamal_registry_stateful_snapshot contract.
pub use self::spl_elgamal_registry_stateful::materialize_elgamal_registry_stateful_snapshot;
/// Migrated read_elgamal_registry_stateful_snapshot contract.
pub use self::spl_elgamal_registry_stateful::read_elgamal_registry_stateful_snapshot;
/// Correlation outcome between one committed instruction fact and one final state snapshot.
pub use self::spl_token_2022_correlation::Token2022CorrelationStatus;
/// Deterministic correlation report for one instruction output and one final snapshot.
pub use self::spl_token_2022_correlation::Token2022StateCorrelationReport;
/// Correlates one materialized instruction fact with one authoritative Token-2022 snapshot.
pub use self::spl_token_2022_correlation::correlate_token_2022_instruction_with_snapshot;
/// Maximum number of distinct proof context-state accounts accepted by one preflight.
pub use self::spl_token_2022_crypto_preflight::MAX_TOKEN_2022_PROOF_CONTEXTS;
/// Token-2022 cryptographic preflight report.
pub use self::spl_token_2022_crypto_preflight::Token2022CryptographicPreflightReport;
/// Token-2022 cryptographic preflight request.
pub use self::spl_token_2022_crypto_preflight::Token2022CryptographicPreflightRequest;
/// One validated proof context report.
pub use self::spl_token_2022_crypto_preflight::Token2022ProofContextReport;
/// One required proof context-state account.
pub use self::spl_token_2022_crypto_preflight::Token2022ProofContextRequirement;
/// Encoded proof context metadata bytes.
pub use self::spl_token_2022_crypto_preflight::ZK_PROOF_CONTEXT_META_BYTES;
/// Inspects bounded Token-2022 cryptographic proof contexts.
pub use self::spl_token_2022_crypto_preflight::inspect_token_2022_cryptographic_preflight;
/// Maximum number of distinct signers accepted by one Token-2022 execution envelope.
pub use self::spl_token_2022_execution_orchestration::MAX_TOKEN_2022_EXECUTION_SIGNERS;
/// One explicit stateful postcondition retained after Token-2022 execution.
pub use self::spl_token_2022_execution_orchestration::Token2022ExecutionPostcondition;
/// Result of one stateful postcondition after Token-2022 execution.
pub use self::spl_token_2022_execution_orchestration::Token2022ExecutionPostconditionStatus;
/// Deterministic Token-2022 execution-readiness report.
pub use self::spl_token_2022_execution_orchestration::Token2022ExecutionReadinessReport;
/// Complete deterministic Token-2022 execution-readiness request.
pub use self::spl_token_2022_execution_orchestration::Token2022ExecutionReadinessRequest;
/// Aggregates Token-2022 postconditions without inventing success.
pub use self::spl_token_2022_execution_orchestration::summarize_token_2022_postconditions;
/// Validates the complete Token-2022 execution envelope before signing.
pub use self::spl_token_2022_execution_orchestration::validate_token_2022_execution_readiness;
/// Maximum Token Metadata bytes accepted from Solana return data.
pub use self::spl_token_2022_metadata::MAX_TOKEN_2022_METADATA_EMIT_BYTES;
/// Bounded evidence extracted from one successful Token Metadata `Emit` simulation.
pub use self::spl_token_2022_metadata::Token2022MetadataEmitEvidence;
/// Checks the final embedded metadata authority against one authoritative snapshot.
pub use self::spl_token_2022_metadata::inspect_token_2022_metadata_authority_postcondition;
/// Extracts and validates Token Metadata return data from one exact simulation.
pub use self::spl_token_2022_metadata::inspect_token_2022_metadata_emit_simulation;
/// Validates one Token Metadata `Emit` range against Solana return-data bounds.
pub use self::spl_token_2022_metadata::validate_token_2022_metadata_emit_range;
/// Migrated MAX_TOKEN_2022_PREFLIGHT_ACCOUNTS contract.
pub use self::spl_token_2022_preflight::MAX_TOKEN_2022_PREFLIGHT_ACCOUNTS;
/// Migrated MAX_TOKEN_2022_PREFLIGHT_TOTAL_BYTES contract.
pub use self::spl_token_2022_preflight::MAX_TOKEN_2022_PREFLIGHT_TOTAL_BYTES;
/// Migrated Token2022PreflightAccountReport contract.
pub use self::spl_token_2022_preflight::Token2022PreflightAccountReport;
/// Migrated Token2022PreflightReport contract.
pub use self::spl_token_2022_preflight::Token2022PreflightReport;
/// Migrated Token2022PreflightRequest contract.
pub use self::spl_token_2022_preflight::Token2022PreflightRequest;
/// Migrated Token2022PreflightRequirement contract.
pub use self::spl_token_2022_preflight::Token2022PreflightRequirement;
/// Migrated inspect_token_2022_preflight contract.
pub use self::spl_token_2022_preflight::inspect_token_2022_preflight;
/// Maximum number of proofs accepted for one Token-2022 operation.
pub use self::spl_token_2022_proof_orchestration::MAX_TOKEN_2022_OPERATION_PROOFS;
/// Token-2022 proof orchestration report.
pub use self::spl_token_2022_proof_orchestration::Token2022ProofOrchestrationReport;
/// Token-2022 proof orchestration request.
pub use self::spl_token_2022_proof_orchestration::Token2022ProofOrchestrationRequest;
/// Validates mixed inline and context-state proof orchestration.
pub use self::spl_token_2022_proof_orchestration::orchestrate_token_2022_proofs;
/// Migrated MAX_TOKEN_2022_STATEFUL_ACCOUNT_BYTES contract.
pub use self::spl_token_2022_stateful::MAX_TOKEN_2022_STATEFUL_ACCOUNT_BYTES;
/// Migrated Token2022StatefulContext contract.
pub use self::spl_token_2022_stateful::Token2022StatefulContext;
/// Migrated Token2022StatefulReadRequest contract.
pub use self::spl_token_2022_stateful::Token2022StatefulReadRequest;
/// Migrated Token2022StatefulReadResult contract.
pub use self::spl_token_2022_stateful::Token2022StatefulReadResult;
/// Migrated Token2022StatefulSnapshotBundle contract.
pub use self::spl_token_2022_stateful::Token2022StatefulSnapshotBundle;
/// Migrated materialize_parsed_token_2022_stateful_snapshot contract.
pub use self::spl_token_2022_stateful::materialize_parsed_token_2022_stateful_snapshot;
/// Migrated materialize_parsed_token_2022_stateful_snapshot_with_context contract.
pub use self::spl_token_2022_stateful::materialize_parsed_token_2022_stateful_snapshot_with_context;
/// Migrated materialize_token_2022_account_info_result contract.
pub use self::spl_token_2022_stateful::materialize_token_2022_account_info_result;
/// Migrated materialize_token_2022_stateful_snapshot contract.
pub use self::spl_token_2022_stateful::materialize_token_2022_stateful_snapshot;
/// Migrated read_token_2022_stateful_snapshot contract.
pub use self::spl_token_2022_stateful::read_token_2022_stateful_snapshot;
/// One machine-readable classic SPL Token stateful check.
pub use self::spl_token_stateful::SplTokenStatefulCheck;
/// One contextual classic SPL Token stateful fact.
pub use self::spl_token_stateful::SplTokenStatefulFact;
/// Complete classic SPL Token stateful readiness report.
pub use self::spl_token_stateful::SplTokenStatefulReadinessReport;
/// Complete request for one classic SPL Token stateful readiness inspection.
pub use self::spl_token_stateful::SplTokenStatefulReadinessRequest;
/// Classic SPL Token stateful readiness outcome.
pub use self::spl_token_stateful::SplTokenStatefulReadinessStatus;
/// Inspects Localnet or Devnet classic SPL Token state required before simulation.
pub use self::spl_token_stateful::inspect_spl_token_stateful_readiness;
/// Canonical tracing target for pipeline orchestration.
pub(crate) use self::constants::TRACING_TARGET;

View File

@@ -0,0 +1,240 @@
// file: ks-pipeline/src/metadata_metaplex_token_metadata_execution_orchestration.rs
// version: 4
//! Simulation-first Metaplex Token Metadata execution orchestration.
/// Maximum number of resolved signers accepted by one Metaplex execution envelope.
pub const MAX_METAPLEX_TOKEN_METADATA_EXECUTION_SIGNERS: usize = 32;
/// Complete request checked before signing or submitting one Metaplex transaction.
#[derive(Clone, Debug, PartialEq)]
pub struct MetaplexTokenMetadataExecutionReadinessRequest {
/// Exact prepared execution plan.
pub plan: ks_lib::ExApiPreparedExecutionPlan,
/// Successful stateful preflight report.
pub preflight: crate::MetaplexTokenMetadataPreflightReport,
/// Exact compiled message hash.
pub message_hash: std::string::String,
/// Hash retained by simulation evidence.
pub simulated_message_hash: std::string::String,
/// Whether simulation was performed.
pub simulated: bool,
/// Whether simulation succeeded.
pub simulation_succeeded: bool,
/// Public keys available to sign.
pub resolved_signers: std::vec::Vec<ks_lib::MdPubkey>,
/// Whether submission was requested.
pub submit: bool,
/// Whether the operator confirmed submission.
pub operator_confirmed: bool,
}
/// Deterministic readiness report emitted before signing.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct MetaplexTokenMetadataExecutionReadinessReport {
/// Stable operation code.
pub operation_code: std::string::String,
/// Exact message hash bound to simulation.
pub message_hash: std::string::String,
/// Confirmed preflight context slot.
pub context_slot: u64,
/// Deduplicated required signers.
pub required_signers: std::vec::Vec<ks_lib::MdPubkey>,
/// Whether signing and submission are authorized.
pub send_authorized: bool,
/// Ordered readiness checks, including a probe-only failed-simulation marker when applicable.
pub checks: std::vec::Vec<std::string::String>,
}
/// Validates preflight, exact simulation evidence, signers and submission policies.
///
/// Failed simulations remain observable only for `submit=false`; submission always requires success.
pub fn validate_metaplex_token_metadata_execution_readiness(
request: &crate::MetaplexTokenMetadataExecutionReadinessRequest,
) -> ks_core::Result<crate::MetaplexTokenMetadataExecutionReadinessReport> {
if request.plan.operation_code != request.preflight.operation_code {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_execution_operation_mismatch",
"Metaplex plan and preflight operation codes must match",
));
}
if request.message_hash.trim().is_empty()
|| request.message_hash != request.simulated_message_hash
|| !request.simulated
{
return std::result::Result::Err(ks_core::Error::new(
"metaplex_execution_simulation_required",
"Metaplex execution requires exact-message simulation evidence",
));
}
if request.submit && !request.simulation_succeeded {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_execution_simulation_required",
"Metaplex submission requires successful exact-message simulation",
));
}
if request.resolved_signers.len() > crate::MAX_METAPLEX_TOKEN_METADATA_EXECUTION_SIGNERS {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_execution_signer_limit_exceeded",
"Metaplex execution signer limit exceeded",
));
}
let resolved = request
.resolved_signers
.iter()
.map(|item| return item.0.clone())
.collect::<std::collections::BTreeSet<std::string::String>>();
let mut required = std::collections::BTreeMap::<std::string::String, ks_lib::MdPubkey>::new();
for signer in &request.plan.required_signers {
if !resolved.contains(signer.pubkey.0.as_str()) {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_execution_signer_unresolved",
format!("Metaplex signer {} is unresolved", signer.pubkey.0),
));
}
required
.entry(signer.pubkey.0.clone())
.or_insert_with(|| return signer.pubkey.clone());
}
if request.submit && !request.operator_confirmed {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_execution_confirmation_required",
"Metaplex submission requires explicit operator confirmation",
));
}
if request.submit && request.plan.policy.dry_run {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_execution_dry_run_blocks_submission",
"Metaplex dry-run plan cannot be submitted",
));
}
return std::result::Result::Ok(crate::MetaplexTokenMetadataExecutionReadinessReport {
operation_code: request.plan.operation_code.clone(),
message_hash: request.message_hash.clone(),
context_slot: request.preflight.context_slot,
required_signers: required.into_values().collect(),
send_authorized: request.submit
&& request.operator_confirmed
&& request.simulation_succeeded
&& !request.plan.policy.dry_run,
checks: vec![
"preflight_bound_to_operation".to_string(),
if request.simulation_succeeded {
"simulation_bound_to_exact_message".to_string()
} else {
"failed_simulation_retained_for_probe_only".to_string()
},
"all_required_signers_resolved".to_string(),
"submission_policy_consistent".to_string(),
],
});
}
/// Result of one explicit postcondition.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum MetaplexTokenMetadataPostconditionStatus {
/// Final state confirms the expected effect.
Confirmed,
/// Final state contradicts the expected effect.
Contradicted,
/// No stateful assertion applies.
NotApplicable,
}
/// Aggregates Metaplex postconditions without turning absence into success.
pub fn summarize_metaplex_token_metadata_postconditions(
statuses: &[crate::MetaplexTokenMetadataPostconditionStatus],
) -> crate::MetaplexTokenMetadataPostconditionStatus {
if statuses
.iter()
.any(|value| return *value == crate::MetaplexTokenMetadataPostconditionStatus::Contradicted)
{
return crate::MetaplexTokenMetadataPostconditionStatus::Contradicted;
}
if statuses
.iter()
.any(|value| return *value == crate::MetaplexTokenMetadataPostconditionStatus::Confirmed)
{
return crate::MetaplexTokenMetadataPostconditionStatus::Confirmed;
}
return crate::MetaplexTokenMetadataPostconditionStatus::NotApplicable;
}
#[cfg(test)]
mod tests {
#[test]
fn failed_simulation_is_reportable_only_without_submission() {
let fee_payer = ks_lib::MdPubkey("11111111111111111111111111111111".to_string());
let plan = ks_lib::ExApiPreparedExecutionPlan {
executor_name: "kb-lib.executor.metadata.metaplex_token_metadata".to_string(),
executor_version: "0.4.8".to_string(),
intent_id: "metaplex-failed-simulation-probe".to_string(),
operation_code: ks_lib::EX_METAPLEX_TOKEN_METADATA_USE_OPERATION.to_string(),
fee_payer: fee_payer.clone(),
instructions: vec![ks_lib::ExApiPlannedInstruction {
program_id: ks_lib::MdProgramId(
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID.to_string(),
),
operation_code: ks_lib::EX_METAPLEX_TOKEN_METADATA_USE_OPERATION.to_string(),
accounts: vec![],
data: vec![51],
}],
required_signers: vec![],
policy: ks_lib::ExApiExecutionPolicy {
dry_run: false,
..ks_lib::ExApiExecutionPolicy::default()
},
requested_spend_lamports: 0,
requested_compute_unit_price_micro_lamports: std::option::Option::None,
};
let request = crate::MetaplexTokenMetadataExecutionReadinessRequest {
plan: plan.clone(),
preflight: crate::MetaplexTokenMetadataPreflightReport {
operation_code: plan.operation_code.clone(),
context_slot: 42,
inspected_accounts: vec![],
checks: vec![],
},
message_hash: "message-hash".to_string(),
simulated_message_hash: "message-hash".to_string(),
simulated: true,
simulation_succeeded: false,
resolved_signers: vec![],
submit: false,
operator_confirmed: false,
};
let report = crate::validate_metaplex_token_metadata_execution_readiness(&request);
assert!(report.is_ok());
if let std::result::Result::Ok(report) = report {
assert!(!report.send_authorized);
assert!(
report
.checks
.iter()
.any(|check| return check == "failed_simulation_retained_for_probe_only")
);
}
let mut submission = request;
submission.submit = true;
submission.operator_confirmed = true;
assert!(crate::validate_metaplex_token_metadata_execution_readiness(&submission).is_err());
}
#[test]
fn postconditions_never_convert_not_applicable_into_success() {
assert_eq!(
crate::summarize_metaplex_token_metadata_postconditions(&[
crate::MetaplexTokenMetadataPostconditionStatus::NotApplicable,
]),
crate::MetaplexTokenMetadataPostconditionStatus::NotApplicable,
);
assert_eq!(
crate::summarize_metaplex_token_metadata_postconditions(&[
crate::MetaplexTokenMetadataPostconditionStatus::Confirmed,
crate::MetaplexTokenMetadataPostconditionStatus::Contradicted,
]),
crate::MetaplexTokenMetadataPostconditionStatus::Contradicted,
);
}
}

View File

@@ -0,0 +1,136 @@
// file: ks-pipeline/src/metadata_metaplex_token_metadata_preflight.rs
// version: 4
//! Stateful Metaplex Token Metadata preflight contracts.
/// Maximum number of correlated Metaplex snapshots accepted by one preflight.
pub const MAX_METAPLEX_TOKEN_METADATA_PREFLIGHT_ACCOUNTS: usize = 32;
/// Complete preflight request for one prepared Metaplex execution plan.
#[derive(Clone, Debug, PartialEq)]
pub struct MetaplexTokenMetadataPreflightRequest {
/// Exact prepared plan produced by `ks-lib`.
pub plan: ks_lib::ExApiPreparedExecutionPlan,
/// Confirmed bounded state snapshots correlated with the plan.
pub snapshots: std::vec::Vec<crate::MetaplexTokenMetadataStatefulReadResult>,
/// Whether deprecated execution was explicitly approved by the operator.
pub allow_deprecated_operation: bool,
}
/// Deterministic Metaplex preflight report.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct MetaplexTokenMetadataPreflightReport {
/// Stable operation code.
pub operation_code: std::string::String,
/// Highest confirmed context slot across supplied snapshots.
pub context_slot: u64,
/// Canonical accounts inspected by the preflight.
pub inspected_accounts: std::vec::Vec<ks_lib::MdPubkey>,
/// Ordered successful checks.
pub checks: std::vec::Vec<std::string::String>,
}
/// Validates program ownership, snapshot bounds, account correlation and deprecation policy.
pub fn inspect_metaplex_token_metadata_preflight(
request: &crate::MetaplexTokenMetadataPreflightRequest,
) -> ks_core::Result<crate::MetaplexTokenMetadataPreflightReport> {
if request.snapshots.len() > crate::MAX_METAPLEX_TOKEN_METADATA_PREFLIGHT_ACCOUNTS {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_preflight_account_limit_exceeded",
"Metaplex preflight account limit exceeded",
));
}
if request.plan.instructions.is_empty() {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_preflight_empty_plan",
"Metaplex execution plan must contain at least one instruction",
));
}
for instruction in &request.plan.instructions {
if instruction.program_id.0 != ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_preflight_program_mismatch",
"Every Metaplex plan instruction must target Token Metadata",
));
}
if instruction.operation_code != request.plan.operation_code {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_preflight_operation_mismatch",
"Metaplex plan and instruction operation codes must match",
));
}
}
let deprecated = ks_lib::EX_METAPLEX_TOKEN_METADATA_DEPRECATED_OPERATION_CODES
.contains(&request.plan.operation_code.as_str());
if deprecated && !request.allow_deprecated_operation {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_preflight_deprecated_operation_not_approved",
"Deprecated Metaplex execution requires explicit operator approval",
));
}
let mut inspected = std::collections::BTreeMap::<std::string::String, ks_lib::MdPubkey>::new();
let mut context_slot = 0_u64;
for snapshot in &request.snapshots {
if snapshot.commitment != "confirmed" {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_preflight_commitment_mismatch",
"Metaplex stateful snapshots must use confirmed commitment",
));
}
context_slot = context_slot.max(snapshot.context_slot);
inspected
.entry(snapshot.snapshot.account.0.clone())
.or_insert_with(|| return snapshot.snapshot.account.clone());
}
return std::result::Result::Ok(crate::MetaplexTokenMetadataPreflightReport {
operation_code: request.plan.operation_code.clone(),
context_slot,
inspected_accounts: inspected.into_values().collect(),
checks: vec![
"program_id_exact".to_string(),
"operation_code_exact".to_string(),
"confirmed_state_snapshots".to_string(),
"deprecated_policy_explicit".to_string(),
"account_correlation_bounded".to_string(),
],
});
}
#[cfg(test)]
mod tests {
#[test]
fn rejects_empty_plans_and_unapproved_deprecated_operations() {
let mut plan = ks_lib::ExApiPreparedExecutionPlan {
executor_name: "ks-lib".to_string(),
executor_version: "0".to_string(),
intent_id: "i".to_string(),
operation_code: ks_lib::EX_METAPLEX_TOKEN_METADATA_PUFF_METADATA_OPERATION.to_string(),
fee_payer: ks_lib::MdPubkey("11111111111111111111111111111111".to_string()),
instructions: vec![],
required_signers: vec![],
policy: std::default::Default::default(),
requested_spend_lamports: 0,
requested_compute_unit_price_micro_lamports: std::option::Option::None,
};
let empty = crate::MetaplexTokenMetadataPreflightRequest {
plan: plan.clone(),
snapshots: vec![],
allow_deprecated_operation: false,
};
assert!(crate::inspect_metaplex_token_metadata_preflight(&empty).is_err());
plan.instructions.push(ks_lib::ExApiPlannedInstruction {
program_id: ks_lib::MdProgramId(
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID.to_string(),
),
operation_code: plan.operation_code.clone(),
accounts: vec![],
data: vec![14],
});
let deprecated = crate::MetaplexTokenMetadataPreflightRequest {
plan,
snapshots: vec![],
allow_deprecated_operation: false,
};
assert!(crate::inspect_metaplex_token_metadata_preflight(&deprecated).is_err());
}
}

View File

@@ -0,0 +1,600 @@
// file: ks-pipeline/src/metadata_metaplex_token_metadata_stateful.rs
// version: 10
//! Bounded Metaplex Token Metadata account reads and canonical state projections.
/// Maximum complete Metaplex account data accepted by one RPC read.
///
/// This bound follows the complete `getAccountInfo` contract of the execution transport. Decoder
/// limits remain independent for already-available offline bytes or future chunked readers.
pub const MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES: usize =
ks_onchain_transport::MAX_COMPLETE_ACCOUNT_DATA_BYTES;
/// Supported Metaplex account category for one bounded stateful read.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum MetaplexTokenMetadataAccountKind {
/// Metadata PDA derived from the mint encoded in the account.
Metadata,
/// Master edition or printed edition PDA derived from the supplied mint.
Edition {
/// Mint used to derive the edition PDA.
mint: ks_lib::MdPubkey,
},
/// Edition marker PDA derived from the master mint and printed edition number.
EditionMarker {
/// Master mint used to derive the edition marker PDA.
mint: ks_lib::MdPubkey,
/// Printed edition number whose marker bit must be decoded.
edition: u64,
},
/// Programmable token record PDA derived from mint and token account.
TokenRecord {
/// Mint used to derive the token-record PDA.
mint: ks_lib::MdPubkey,
/// Token account used to derive the token-record PDA.
token: ks_lib::MdPubkey,
},
/// Token Owned Escrow PDA whose authority seeds and bump are self-validated by the decoder.
TokenOwnedEscrow,
}
/// One bounded Metaplex account read request.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct MetaplexTokenMetadataStatefulReadRequest {
/// Endpoint role used for the HTTP RPC request.
pub query_role: std::string::String,
/// Canonical account address.
pub account: ks_lib::MdPubkey,
/// Expected account category and derivation inputs.
pub kind: crate::MetaplexTokenMetadataAccountKind,
/// 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,
}
impl crate::MetaplexTokenMetadataStatefulReadRequest {
/// Validates the bounded complete-account RPC request.
pub fn validate(&self) -> ks_core::Result<()> {
if self.query_role.trim().is_empty() {
return std::result::Result::Err(ks_core::Error::config(
"Metaplex stateful read query_role must not be empty",
));
}
let account_result = ks_onchain_transport::validate_solana_pubkey_text(
self.account.0.as_str(),
"Metaplex stateful account",
);
if let std::result::Result::Err(error) = account_result {
return std::result::Result::Err(error);
}
let derivation_result = match &self.kind {
crate::MetaplexTokenMetadataAccountKind::Metadata => std::result::Result::Ok(()),
crate::MetaplexTokenMetadataAccountKind::Edition { mint }
| crate::MetaplexTokenMetadataAccountKind::EditionMarker { mint, .. } => {
ks_onchain_transport::validate_solana_pubkey_text(
mint.0.as_str(),
"Metaplex stateful derivation mint",
)
},
crate::MetaplexTokenMetadataAccountKind::TokenRecord { mint, token } => {
let mint_result = ks_onchain_transport::validate_solana_pubkey_text(
mint.0.as_str(),
"Metaplex stateful derivation mint",
);
if let std::result::Result::Err(error) = mint_result {
return std::result::Result::Err(error);
}
ks_onchain_transport::validate_solana_pubkey_text(
token.0.as_str(),
"Metaplex stateful derivation token",
)
},
crate::MetaplexTokenMetadataAccountKind::TokenOwnedEscrow => {
std::result::Result::Ok(())
},
};
if let std::result::Result::Err(error) = derivation_result {
return std::result::Result::Err(error);
}
if self.max_data_bytes == 0
|| self.max_data_bytes > crate::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES
{
return std::result::Result::Err(ks_core::Error::config(format!(
"Metaplex stateful max_data_bytes must be between 1 and {}",
crate::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES
)));
}
return std::result::Result::Ok(());
}
}
/// Canonical bounded Metaplex state snapshot.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct MetaplexTokenMetadataStatefulSnapshot {
/// Canonical account address.
pub account: ks_lib::MdPubkey,
/// Stable account category.
pub account_kind: std::string::String,
/// Mint correlated with this account when available.
pub mint: std::option::Option<ks_lib::MdPubkey>,
/// RPC context slot.
pub slot: u64,
/// Bounded decoder-owned projection.
pub payload_json: serde_json::Value,
}
/// One bounded RPC read and its validated snapshot.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct MetaplexTokenMetadataStatefulReadResult {
/// Commitment used by the read.
pub commitment: std::string::String,
/// Context slot returned by the endpoint.
pub context_slot: u64,
/// Validated canonical snapshot.
pub snapshot: crate::MetaplexTokenMetadataStatefulSnapshot,
}
/// Reads and validates one bounded Metaplex Token Metadata account.
pub async fn read_metaplex_token_metadata_stateful_snapshot(
pool: &ks_onchain_transport::HttpEndpointPool,
request: &crate::MetaplexTokenMetadataStatefulReadRequest,
) -> ks_core::Result<crate::MetaplexTokenMetadataStatefulReadResult> {
if let std::result::Result::Err(error) = request.validate() {
return std::result::Result::Err(error);
}
let config = match ks_onchain_transport::GetAccountInfoConfig::new_with_data(
ks_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_metaplex_token_metadata_account_info_result(request, &result);
}
/// Validates one RPC account response and delegates parsing to `ks-lib`.
pub fn materialize_metaplex_token_metadata_account_info_result(
request: &crate::MetaplexTokenMetadataStatefulReadRequest,
result: &ks_onchain_transport::AccountInfoResult,
) -> ks_core::Result<crate::MetaplexTokenMetadataStatefulReadResult> {
if let std::result::Result::Err(error) = request.validate() {
return std::result::Result::Err(error);
}
if let std::option::Option::Some(minimum) = request.min_context_slot {
if result.context.slot < minimum {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_stateful_context_slot_too_old",
"Metaplex account context slot is below the requested minimum",
));
}
}
let account = match result.account.as_ref() {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_stateful_account_missing",
format!("Metaplex account {} does not exist", request.account.0),
));
},
};
if account.executable {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_stateful_account_executable",
"Metaplex state accounts must not be executable",
));
}
if account.owner.0 != ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_stateful_owner_mismatch",
"Metaplex state account owner does not match Token Metadata",
));
}
if account.space != account.data.len() as u64 || account.data.len() > request.max_data_bytes {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_stateful_account_data_invalid",
"Metaplex account data is incomplete or above the configured bound",
));
}
let snapshot = match decode_snapshot(request, result.context.slot, account.data.as_slice()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(crate::MetaplexTokenMetadataStatefulReadResult {
commitment: "confirmed".to_string(),
context_slot: result.context.slot,
snapshot,
});
}
fn stateful_decode_error(
request: &crate::MetaplexTokenMetadataStatefulReadRequest,
error: ks_lib::DcMetadataMtmMetadataAccountDecodeError,
) -> ks_core::Error {
return ks_core::Error::new(
"metaplex_stateful_decode_failed",
format!(
"Metaplex stateful decode failed for account {} kind {:?}: {error}",
request.account.0, request.kind
),
);
}
fn decode_snapshot(
request: &crate::MetaplexTokenMetadataStatefulReadRequest,
slot: u64,
data: &[u8],
) -> ks_core::Result<crate::MetaplexTokenMetadataStatefulSnapshot> {
let owner = ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID;
return match &request.kind {
crate::MetaplexTokenMetadataAccountKind::Metadata => {
match ks_lib::decoder_metadata_metaplex_token_metadata_decode_metadata_account(
request.account.0.as_str(),
owner,
data,
) {
std::result::Result::Ok(value) => {
std::result::Result::Ok(crate::MetaplexTokenMetadataStatefulSnapshot {
account: request.account.clone(),
account_kind: "metadata".to_string(),
mint: std::option::Option::Some(ks_lib::MdPubkey(value.mint)),
slot,
payload_json: value.payload_json,
})
},
std::result::Result::Err(error) => {
std::result::Result::Err(stateful_decode_error(request, error))
},
}
},
crate::MetaplexTokenMetadataAccountKind::Edition { mint } => {
match ks_lib::decoder_metadata_metaplex_token_metadata_decode_edition_account(
request.account.0.as_str(),
owner,
mint.0.as_str(),
data,
) {
std::result::Result::Ok(value) => {
std::result::Result::Ok(crate::MetaplexTokenMetadataStatefulSnapshot {
account: request.account.clone(),
account_kind: "edition".to_string(),
mint: std::option::Option::Some(mint.clone()),
slot,
payload_json: value.payload_json,
})
},
std::result::Result::Err(error) => {
std::result::Result::Err(stateful_decode_error(request, error))
},
}
},
crate::MetaplexTokenMetadataAccountKind::EditionMarker { mint, edition } => {
match ks_lib::decoder_metadata_metaplex_token_metadata_decode_edition_marker_account(
request.account.0.as_str(),
owner,
mint.0.as_str(),
*edition,
data,
) {
std::result::Result::Ok(value) => {
std::result::Result::Ok(crate::MetaplexTokenMetadataStatefulSnapshot {
account: request.account.clone(),
account_kind: "edition_marker".to_string(),
mint: std::option::Option::Some(mint.clone()),
slot,
payload_json: serde_json::json!({
"account": value.account,
"bump": value.bump,
"mint": value.mint,
"kind": match value.kind {
ks_lib::DcMetadataMtmEditionMarkerAccountKind::EditionMarkerV1 => "edition_marker_v1",
ks_lib::DcMetadataMtmEditionMarkerAccountKind::EditionMarkerV2 => "edition_marker_v2",
},
"marker_group": value.marker_group,
"edition": value.edition,
"byte_index": value.byte_index,
"bit_mask": value.bit_mask,
"edition_taken": value.edition_taken,
"ledger": value.ledger,
"payload": value.payload_json,
}),
})
},
std::result::Result::Err(error) => {
std::result::Result::Err(stateful_decode_error(request, error))
},
}
},
crate::MetaplexTokenMetadataAccountKind::TokenRecord { mint, token } => {
match ks_lib::decoder_metadata_metaplex_token_metadata_decode_token_record_account(
request.account.0.as_str(),
owner,
mint.0.as_str(),
token.0.as_str(),
data,
) {
std::result::Result::Ok(value) => {
std::result::Result::Ok(crate::MetaplexTokenMetadataStatefulSnapshot {
account: request.account.clone(),
account_kind: "token_record".to_string(),
mint: std::option::Option::Some(mint.clone()),
slot,
payload_json: value.payload_json,
})
},
std::result::Result::Err(error) => {
std::result::Result::Err(stateful_decode_error(request, error))
},
}
},
crate::MetaplexTokenMetadataAccountKind::TokenOwnedEscrow => {
match ks_lib::decoder_metadata_metaplex_token_metadata_decode_token_owned_escrow_account(
request.account.0.as_str(),
owner,
data,
) {
std::result::Result::Ok(value) => {
let authority_kind = match value.authority_kind {
ks_lib::DcMetadataMtmTokenOwnedEscrowAuthorityKind::TokenOwner => {
"token_owner"
},
ks_lib::DcMetadataMtmTokenOwnedEscrowAuthorityKind::Creator => "creator",
};
std::result::Result::Ok(crate::MetaplexTokenMetadataStatefulSnapshot {
account: request.account.clone(),
account_kind: "token_owned_escrow".to_string(),
mint: std::option::Option::Some(ks_lib::MdPubkey(value.base_token.clone())),
slot,
payload_json: serde_json::json!({
"account": value.account,
"bump": value.bump,
"base_token": value.base_token,
"authority_kind": authority_kind,
"creator": value.creator,
"stored_bump": value.stored_bump,
"payload": value.payload_json,
}),
})
},
std::result::Result::Err(error) => {
std::result::Result::Err(stateful_decode_error(request, error))
},
}
},
};
}
#[cfg(test)]
mod tests {
#[test]
fn stateful_read_request_round_trips_through_json() {
let request = crate::MetaplexTokenMetadataStatefulReadRequest {
query_role: "rpc".to_string(),
account: ks_lib::MdPubkey("11111111111111111111111111111111".to_string()),
kind: crate::MetaplexTokenMetadataAccountKind::Metadata,
min_context_slot: std::option::Option::Some(42),
max_data_bytes: 1024,
};
let json = match serde_json::to_string(&request) {
std::result::Result::Ok(json) => json,
std::result::Result::Err(error) => panic!("request serialization failed: {error}"),
};
let decoded = match serde_json::from_str::<crate::MetaplexTokenMetadataStatefulReadRequest>(
json.as_str(),
) {
std::result::Result::Ok(decoded) => decoded,
std::result::Result::Err(error) => panic!("request deserialization failed: {error}"),
};
assert_eq!(decoded, request);
let marker = crate::MetaplexTokenMetadataStatefulReadRequest {
query_role: "rpc".to_string(),
account: ks_lib::MdPubkey("11111111111111111111111111111111".to_string()),
kind: crate::MetaplexTokenMetadataAccountKind::EditionMarker {
mint: ks_lib::MdPubkey("11111111111111111111111111111111".to_string()),
edition: 1,
},
min_context_slot: std::option::Option::Some(43),
max_data_bytes: 1024,
};
let marker_json = match serde_json::to_string(&marker) {
std::result::Result::Ok(json) => json,
std::result::Result::Err(error) => {
panic!("marker request serialization failed: {error}")
},
};
let marker_decoded = match serde_json::from_str::<
crate::MetaplexTokenMetadataStatefulReadRequest,
>(marker_json.as_str())
{
std::result::Result::Ok(decoded) => decoded,
std::result::Result::Err(error) => {
panic!("marker request deserialization failed: {error}")
},
};
assert_eq!(marker_decoded, marker);
let escrow = crate::MetaplexTokenMetadataStatefulReadRequest {
query_role: "rpc".to_string(),
account: ks_lib::MdPubkey("11111111111111111111111111111111".to_string()),
kind: crate::MetaplexTokenMetadataAccountKind::TokenOwnedEscrow,
min_context_slot: std::option::Option::Some(44),
max_data_bytes: 1024,
};
let escrow_json = match serde_json::to_string(&escrow) {
std::result::Result::Ok(json) => json,
std::result::Result::Err(error) => {
panic!("escrow request serialization failed: {error}")
},
};
let escrow_decoded = match serde_json::from_str::<
crate::MetaplexTokenMetadataStatefulReadRequest,
>(escrow_json.as_str())
{
std::result::Result::Ok(decoded) => decoded,
std::result::Result::Err(error) => {
panic!("escrow request deserialization failed: {error}")
},
};
assert_eq!(escrow_decoded, escrow);
let invalid_marker = crate::MetaplexTokenMetadataStatefulReadRequest {
query_role: "rpc".to_string(),
account: ks_lib::MdPubkey("11111111111111111111111111111111".to_string()),
kind: crate::MetaplexTokenMetadataAccountKind::EditionMarker {
mint: ks_lib::MdPubkey("invalid".to_string()),
edition: 1,
},
min_context_slot: std::option::Option::None,
max_data_bytes: 1024,
};
assert!(invalid_marker.validate().is_err());
}
#[test]
fn rejects_zero_and_oversize_bounds_before_account_decoding() {
let request = crate::MetaplexTokenMetadataStatefulReadRequest {
query_role: "rpc".to_string(),
account: ks_lib::MdPubkey("11111111111111111111111111111111".to_string()),
kind: crate::MetaplexTokenMetadataAccountKind::Metadata,
min_context_slot: std::option::Option::None,
max_data_bytes: 0,
};
let result = ks_onchain_transport::AccountInfoResult {
context: ks_onchain_transport::RpcResponseContext {
slot: 1,
api_version: std::option::Option::None,
},
account: std::option::Option::None,
};
assert!(
crate::materialize_metaplex_token_metadata_account_info_result(&request, &result)
.is_err()
);
let mut oversized = request;
oversized.max_data_bytes = ks_onchain_transport::MAX_COMPLETE_ACCOUNT_DATA_BYTES + 1;
assert!(oversized.validate().is_err());
}
#[test]
fn token_owned_escrow_materializes_with_canonical_authority_projection() {
let program = match ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID
.parse::<solana_pubkey::Pubkey>()
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("program parse failed: {error}"),
};
let mint = solana_pubkey::Pubkey::new_from_array([6_u8; 32]);
let (account, bump) = solana_pubkey::Pubkey::find_program_address(
&[b"metadata", program.as_ref(), mint.as_ref(), &[0], b"escrow"],
&program,
);
let mut data = std::vec![10_u8];
data.extend_from_slice(mint.as_ref());
data.push(0);
data.push(bump);
assert_eq!(data.len(), 35);
let request = crate::MetaplexTokenMetadataStatefulReadRequest {
query_role: "rpc".to_string(),
account: ks_lib::MdPubkey(account.to_string()),
kind: crate::MetaplexTokenMetadataAccountKind::TokenOwnedEscrow,
min_context_slot: std::option::Option::Some(11),
max_data_bytes: 1024,
};
let result = ks_onchain_transport::AccountInfoResult {
context: ks_onchain_transport::RpcResponseContext {
slot: 11,
api_version: std::option::Option::None,
},
account: std::option::Option::Some(ks_onchain_transport::AccountInfoValue {
lamports: 1_000,
owner: ks_lib::MdProgramId(program.to_string()),
executable: false,
rent_epoch: 0,
space: data.len() as u64,
data,
}),
};
let output =
crate::materialize_metaplex_token_metadata_account_info_result(&request, &result);
let snapshot = match output {
std::result::Result::Ok(value) => value.snapshot,
std::result::Result::Err(error) => panic!("escrow stateful decode failed: {error}"),
};
assert_eq!(snapshot.account_kind, "token_owned_escrow");
assert_eq!(snapshot.mint, std::option::Option::Some(ks_lib::MdPubkey(mint.to_string())));
assert_eq!(snapshot.payload_json["base_token"], serde_json::json!(mint.to_string()));
assert_eq!(snapshot.payload_json["authority_kind"], serde_json::json!("token_owner"));
assert_eq!(snapshot.payload_json["creator"], serde_json::Value::Null);
assert_eq!(snapshot.payload_json["stored_bump"], serde_json::json!(bump));
}
#[test]
fn compact_printed_edition_account_materializes_from_current_network_allocation() {
let program = match ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID
.parse::<solana_pubkey::Pubkey>()
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("program parse failed: {error}"),
};
let mint = solana_pubkey::Pubkey::new_from_array([7_u8; 32]);
let parent = solana_pubkey::Pubkey::new_from_array([8_u8; 32]);
let (account, _) = solana_pubkey::Pubkey::find_program_address(
&[b"metadata", program.as_ref(), mint.as_ref(), b"edition"],
&program,
);
let mut data = vec![1_u8];
data.extend_from_slice(parent.as_ref());
data.extend_from_slice(&1_u64.to_le_bytes());
data.push(0);
assert_eq!(data.len(), 42);
let request = crate::MetaplexTokenMetadataStatefulReadRequest {
query_role: "rpc".to_string(),
account: ks_lib::MdPubkey(account.to_string()),
kind: crate::MetaplexTokenMetadataAccountKind::Edition {
mint: ks_lib::MdPubkey(mint.to_string()),
},
min_context_slot: std::option::Option::Some(12),
max_data_bytes: 1024,
};
let result = ks_onchain_transport::AccountInfoResult {
context: ks_onchain_transport::RpcResponseContext {
slot: 12,
api_version: std::option::Option::None,
},
account: std::option::Option::Some(ks_onchain_transport::AccountInfoValue {
lamports: 1_000,
owner: ks_lib::MdProgramId(program.to_string()),
executable: false,
rent_epoch: 0,
space: 42,
data,
}),
};
let output =
crate::materialize_metaplex_token_metadata_account_info_result(&request, &result);
let snapshot = match output {
std::result::Result::Ok(value) => value.snapshot,
std::result::Result::Err(error) => {
panic!("compact edition stateful decode failed: {error}")
},
};
assert_eq!(snapshot.account_kind, "edition");
assert_eq!(snapshot.payload_json["edition"], serde_json::json!(1));
assert_eq!(snapshot.payload_json["parent"], serde_json::json!(parent.to_string()));
}
#[test]
fn stateful_complete_read_bound_matches_the_transport_contract() {
assert_eq!(
crate::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES,
ks_onchain_transport::MAX_COMPLETE_ACCOUNT_DATA_BYTES
);
}
}

View File

@@ -0,0 +1,831 @@
// file: ks-pipeline/src/metadata_solana_program_execution_orchestration.rs
// version: 3
//! Simulation-first Solana Program Metadata execution orchestration and postconditions.
/// Maximum number of resolved signers accepted by one Program Metadata envelope.
pub const MAX_SOLANA_PROGRAM_METADATA_EXECUTION_SIGNERS: usize = 16;
/// Complete request checked before signing or submitting one Program Metadata transaction.
#[derive(Clone, Debug, PartialEq)]
pub struct SolanaProgramMetadataExecutionReadinessRequest {
/// Exact prepared execution plan.
pub plan: ks_lib::ExApiPreparedExecutionPlan,
/// Successful stateful preflight report.
pub preflight: crate::SolanaProgramMetadataPreflightReport,
/// Exact compiled message hash.
pub message_hash: std::string::String,
/// Hash retained by simulation evidence.
pub simulated_message_hash: std::string::String,
/// Whether simulation was performed.
pub simulated: bool,
/// Whether simulation succeeded.
pub simulation_succeeded: bool,
/// Public keys available to sign.
pub resolved_signers: std::vec::Vec<ks_lib::MdPubkey>,
/// Whether submission was requested.
pub submit: bool,
/// Whether the operator confirmed submission.
pub operator_confirmed: bool,
}
/// Deterministic readiness report emitted before signing.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct SolanaProgramMetadataExecutionReadinessReport {
/// Stable operation code.
pub operation_code: std::string::String,
/// Exact message hash bound to simulation.
pub message_hash: std::string::String,
/// Confirmed preflight context slot.
pub context_slot: u64,
/// Deduplicated required signers.
pub required_signers: std::vec::Vec<ks_lib::MdPubkey>,
/// Whether signing and submission are authorized.
pub send_authorized: bool,
/// Ordered successful checks.
pub checks: std::vec::Vec<std::string::String>,
}
/// Result of one Program Metadata postcondition.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SolanaProgramMetadataPostconditionStatus {
/// Final state confirms the expected effect.
Confirmed,
/// Final state contradicts the expected effect.
Contradicted,
/// No stateful assertion applies.
NotApplicable,
}
/// One machine-readable Program Metadata postcondition.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct SolanaProgramMetadataPostcondition {
/// Stable check code.
pub code: std::string::String,
/// Account inspected by the check.
pub account: ks_lib::MdPubkey,
/// Terminal status.
pub status: crate::SolanaProgramMetadataPostconditionStatus,
/// Human-readable diagnostic.
pub diagnostic: std::string::String,
}
/// Complete request for one confirmed Program Metadata post-execution inspection.
#[derive(Clone, Debug, PartialEq)]
pub struct SolanaProgramMetadataPostExecutionRequest {
/// Exact operation that was submitted.
pub operation: ks_lib::ExMetadataSpmOperation,
/// Confirmed snapshots captured before execution.
pub before: std::vec::Vec<crate::SolanaProgramMetadataStatefulReadResult>,
/// Confirmed snapshots captured after execution.
pub after: std::vec::Vec<crate::SolanaProgramMetadataStatefulReadResult>,
}
/// Aggregated Program Metadata post-execution report.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct SolanaProgramMetadataPostExecutionReport {
/// Stable operation code.
pub operation_code: std::string::String,
/// Aggregate status without invented success.
pub status: crate::SolanaProgramMetadataPostconditionStatus,
/// Individual postconditions.
pub postconditions: std::vec::Vec<crate::SolanaProgramMetadataPostcondition>,
}
/// Validates simulation, preflight, signer and submission policies.
pub fn validate_solana_program_metadata_execution_readiness(
request: &crate::SolanaProgramMetadataExecutionReadinessRequest,
) -> ks_core::Result<crate::SolanaProgramMetadataExecutionReadinessReport> {
if request.plan.operation_code != request.preflight.operation_code {
return std::result::Result::Err(ks_core::Error::new(
"solana_program_metadata_execution_operation_mismatch",
"Program Metadata plan and preflight operation codes must match",
));
}
if request.preflight.safety.decision == ks_lib::ExSafetyDecision::Deny {
return std::result::Result::Err(ks_core::Error::new(
"solana_program_metadata_execution_safety_denied",
"Program Metadata preflight safety decision denied execution",
));
}
if request.preflight.safety.decision == ks_lib::ExSafetyDecision::RequireConfirmation
&& !request.operator_confirmed
{
return std::result::Result::Err(ks_core::Error::new(
"solana_program_metadata_execution_safety_confirmation_required",
"Program Metadata safety policy requires explicit operator confirmation",
));
}
if request.message_hash.trim().is_empty()
|| request.message_hash != request.simulated_message_hash
|| !request.simulated
|| !request.simulation_succeeded
{
return std::result::Result::Err(ks_core::Error::new(
"solana_program_metadata_execution_simulation_required",
"Program Metadata execution requires successful exact-message simulation",
));
}
if request.resolved_signers.len() > crate::MAX_SOLANA_PROGRAM_METADATA_EXECUTION_SIGNERS {
return std::result::Result::Err(ks_core::Error::new(
"solana_program_metadata_execution_signer_limit_exceeded",
"Program Metadata execution signer limit exceeded",
));
}
let resolved = request
.resolved_signers
.iter()
.map(|item| return item.0.clone())
.collect::<std::collections::BTreeSet<std::string::String>>();
let mut required = std::collections::BTreeMap::<std::string::String, ks_lib::MdPubkey>::new();
for signer in &request.plan.required_signers {
if !resolved.contains(signer.pubkey.0.as_str()) {
return std::result::Result::Err(ks_core::Error::new(
"solana_program_metadata_execution_signer_unresolved",
format!("Program Metadata signer {} is unresolved", signer.pubkey.0),
));
}
required
.entry(signer.pubkey.0.clone())
.or_insert_with(|| return signer.pubkey.clone());
}
if request.submit && !request.operator_confirmed {
return std::result::Result::Err(ks_core::Error::new(
"solana_program_metadata_execution_confirmation_required",
"Program Metadata submission requires explicit operator confirmation",
));
}
if request.submit && request.plan.policy.dry_run {
return std::result::Result::Err(ks_core::Error::new(
"solana_program_metadata_execution_dry_run_blocks_submission",
"Program Metadata dry-run plan cannot be submitted",
));
}
tracing::debug!(
target: crate::TRACING_TARGET,
action = "validate_solana_program_metadata_execution_readiness",
operation_code = %request.plan.operation_code,
context_slot = request.preflight.context_slot,
signer_count = required.len(),
submit = request.submit,
send_authorized = request.submit && request.operator_confirmed && !request.plan.policy.dry_run,
"validated Solana Program Metadata execution readiness"
);
return std::result::Result::Ok(crate::SolanaProgramMetadataExecutionReadinessReport {
operation_code: request.plan.operation_code.clone(),
message_hash: request.message_hash.clone(),
context_slot: request.preflight.context_slot,
required_signers: required.into_values().collect(),
send_authorized: request.submit
&& request.operator_confirmed
&& !request.plan.policy.dry_run,
checks: vec![
"preflight_bound_to_operation".to_string(),
"common_safety_allowed".to_string(),
"simulation_bound_to_exact_message".to_string(),
"all_required_signers_resolved".to_string(),
"submission_policy_consistent".to_string(),
],
});
}
/// Inspects confirmed state after one Program Metadata execution.
pub fn inspect_solana_program_metadata_post_execution(
request: &crate::SolanaProgramMetadataPostExecutionRequest,
) -> ks_core::Result<crate::SolanaProgramMetadataPostExecutionReport> {
if let std::result::Result::Err(error) =
validate_snapshot_set(request.before.as_slice(), "before")
{
return std::result::Result::Err(error);
}
if let std::result::Result::Err(error) =
validate_snapshot_set(request.after.as_slice(), "after")
{
return std::result::Result::Err(error);
}
let target = operation_target(&request.operation).clone();
let before = find_snapshot(request.before.as_slice(), &target);
let after = find_snapshot(request.after.as_slice(), &target);
let postcondition = evaluate_operation_postcondition(
&request.operation,
before,
after,
request.before.as_slice(),
);
let status =
summarize_solana_program_metadata_postconditions(std::slice::from_ref(&postcondition));
tracing::debug!(
target: crate::TRACING_TARGET,
action = "inspect_solana_program_metadata_post_execution",
operation_code = request.operation.operation_code(),
target_account = %target.0,
status = ?status,
"inspected Solana Program Metadata post-execution state"
);
return std::result::Result::Ok(crate::SolanaProgramMetadataPostExecutionReport {
operation_code: request.operation.operation_code().to_string(),
status,
postconditions: vec![postcondition],
});
}
/// Aggregates Program Metadata postconditions without converting absence into success.
pub fn summarize_solana_program_metadata_postconditions(
postconditions: &[crate::SolanaProgramMetadataPostcondition],
) -> crate::SolanaProgramMetadataPostconditionStatus {
if postconditions.iter().any(|item| {
return item.status == crate::SolanaProgramMetadataPostconditionStatus::Contradicted;
}) {
return crate::SolanaProgramMetadataPostconditionStatus::Contradicted;
}
if postconditions.iter().any(|item| {
return item.status == crate::SolanaProgramMetadataPostconditionStatus::Confirmed;
}) {
return crate::SolanaProgramMetadataPostconditionStatus::Confirmed;
}
return crate::SolanaProgramMetadataPostconditionStatus::NotApplicable;
}
fn validate_snapshot_set(
snapshots: &[crate::SolanaProgramMetadataStatefulReadResult],
phase: &str,
) -> ks_core::Result<()> {
if snapshots.len() > crate::MAX_SOLANA_PROGRAM_METADATA_PREFLIGHT_ACCOUNTS {
return std::result::Result::Err(ks_core::Error::new(
"solana_program_metadata_post_execution_account_limit_exceeded",
format!("Program Metadata {phase} snapshot limit exceeded"),
));
}
let mut accounts = std::collections::BTreeSet::new();
for snapshot in snapshots {
if snapshot.commitment != "confirmed" {
return std::result::Result::Err(ks_core::Error::new(
"solana_program_metadata_post_execution_commitment_mismatch",
format!("Program Metadata {phase} snapshots must use confirmed commitment"),
));
}
if !accounts.insert(snapshot.account.0.as_str()) {
return std::result::Result::Err(ks_core::Error::new(
"solana_program_metadata_post_execution_duplicate_account",
format!("duplicate Program Metadata {phase} snapshot for {}", snapshot.account.0),
));
}
}
return std::result::Result::Ok(());
}
fn operation_target(operation: &ks_lib::ExMetadataSpmOperation) -> &ks_lib::MdPubkey {
return match operation {
ks_lib::ExMetadataSpmOperation::Write { buffer, .. }
| ks_lib::ExMetadataSpmOperation::Allocate { buffer, .. } => buffer,
ks_lib::ExMetadataSpmOperation::Initialize { metadata, .. }
| ks_lib::ExMetadataSpmOperation::SetData { metadata, .. }
| ks_lib::ExMetadataSpmOperation::SetImmutable { metadata, .. } => metadata,
ks_lib::ExMetadataSpmOperation::SetAuthority { account, .. }
| ks_lib::ExMetadataSpmOperation::Trim { account, .. }
| ks_lib::ExMetadataSpmOperation::Close { account, .. }
| ks_lib::ExMetadataSpmOperation::Extend { account, .. } => account,
};
}
fn find_snapshot<'a>(
snapshots: &'a [crate::SolanaProgramMetadataStatefulReadResult],
account: &ks_lib::MdPubkey,
) -> std::option::Option<&'a crate::SolanaProgramMetadataStatefulReadResult> {
return snapshots.iter().find(|value| return value.account == *account);
}
fn evaluate_operation_postcondition(
operation: &ks_lib::ExMetadataSpmOperation,
before: std::option::Option<&crate::SolanaProgramMetadataStatefulReadResult>,
after: std::option::Option<&crate::SolanaProgramMetadataStatefulReadResult>,
before_all: &[crate::SolanaProgramMetadataStatefulReadResult],
) -> crate::SolanaProgramMetadataPostcondition {
let target = operation_target(operation).clone();
let confirmed = match operation {
ks_lib::ExMetadataSpmOperation::Write { offset, source, .. } => {
write_postcondition(*offset, source, after, before_all)
},
ks_lib::ExMetadataSpmOperation::Initialize {
program,
canonical,
seed,
encoding,
compression,
format,
data_source,
data,
..
} => initialize_postcondition(
program,
*canonical,
seed.as_slice(),
*encoding,
*compression,
*format,
*data_source,
data.as_ref(),
after,
),
ks_lib::ExMetadataSpmOperation::SetAuthority { new_authority, .. } => {
authority_postcondition(new_authority.as_ref(), after)
},
ks_lib::ExMetadataSpmOperation::SetData {
encoding, compression, format, source, ..
} => set_data_postcondition(
*encoding,
*compression,
*format,
source,
before,
after,
before_all,
),
ks_lib::ExMetadataSpmOperation::SetImmutable { .. } => match after {
std::option::Option::Some(value) => matches!(
&value.state,
crate::SolanaProgramMetadataObservedAccountState::Metadata(metadata) if !metadata.mutable
),
std::option::Option::None => false,
},
ks_lib::ExMetadataSpmOperation::Trim { .. } => trim_postcondition(before, after),
ks_lib::ExMetadataSpmOperation::Close { .. } => match after {
std::option::Option::Some(value) => {
matches!(&value.state, crate::SolanaProgramMetadataObservedAccountState::Missing)
},
std::option::Option::None => false,
},
ks_lib::ExMetadataSpmOperation::Allocate { authority, seed, canonical, .. } => {
allocate_postcondition(authority, seed.as_deref(), *canonical, after)
},
ks_lib::ExMetadataSpmOperation::Extend { length, .. } => {
extend_postcondition(*length, before, after)
},
};
let status = if after.is_none() {
crate::SolanaProgramMetadataPostconditionStatus::NotApplicable
} else if confirmed {
crate::SolanaProgramMetadataPostconditionStatus::Confirmed
} else {
crate::SolanaProgramMetadataPostconditionStatus::Contradicted
};
return crate::SolanaProgramMetadataPostcondition {
code: format!("{}.state", operation.operation_code()),
account: target,
status,
diagnostic: match status {
crate::SolanaProgramMetadataPostconditionStatus::Confirmed => {
"confirmed state matches the expected Program Metadata effect".to_string()
},
crate::SolanaProgramMetadataPostconditionStatus::Contradicted => {
"confirmed state contradicts the expected Program Metadata effect".to_string()
},
crate::SolanaProgramMetadataPostconditionStatus::NotApplicable => {
"no confirmed after-state snapshot was supplied".to_string()
},
},
};
}
fn write_postcondition(
offset: u32,
source: &ks_lib::ExMetadataSpmWriteSource,
after: std::option::Option<&crate::SolanaProgramMetadataStatefulReadResult>,
before_all: &[crate::SolanaProgramMetadataStatefulReadResult],
) -> bool {
let buffer = match after.map(|value| return &value.state) {
std::option::Option::Some(crate::SolanaProgramMetadataObservedAccountState::Buffer(
value,
)) => value,
_ => return false,
};
let start = match usize::try_from(offset) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return false,
};
let expected = match source {
ks_lib::ExMetadataSpmWriteSource::Inline { data } => data.as_slice(),
ks_lib::ExMetadataSpmWriteSource::Buffer { source_buffer } => {
let source_snapshot = match find_snapshot(before_all, source_buffer) {
std::option::Option::Some(value) => value,
std::option::Option::None => return false,
};
match &source_snapshot.state {
crate::SolanaProgramMetadataObservedAccountState::Buffer(value) => {
value.data.as_slice()
},
_ => return false,
}
},
};
let end = match start.checked_add(expected.len()) {
std::option::Option::Some(value) => value,
std::option::Option::None => return false,
};
return buffer.data.get(start..end) == std::option::Option::Some(expected);
}
#[allow(clippy::too_many_arguments)]
fn initialize_postcondition(
program: &ks_lib::MdPubkey,
canonical: bool,
seed: &[u8],
encoding: ks_lib::ExMetadataSpmEncoding,
compression: ks_lib::ExMetadataSpmCompression,
format: ks_lib::ExMetadataSpmFormat,
data_source: ks_lib::ExMetadataSpmDataSource,
data: std::option::Option<&ks_lib::ExMetadataSpmDataInput>,
after: std::option::Option<&crate::SolanaProgramMetadataStatefulReadResult>,
) -> bool {
let metadata = match after.map(|value| return &value.state) {
std::option::Option::Some(crate::SolanaProgramMetadataObservedAccountState::Metadata(
value,
)) => value,
_ => return false,
};
if metadata.program.as_str() != program.0.as_str()
|| metadata.canonical != canonical
|| &metadata.seed.as_bytes()[..] != seed
|| metadata.encoding.wire_value() != encoding.wire_value()
|| metadata.compression.wire_value() != compression.wire_value()
|| metadata.format.wire_value() != format.wire_value()
{
return false;
}
let expected_source = match data {
std::option::Option::Some(value) => value.data_source(),
std::option::Option::None => data_source,
};
if metadata.data_source.wire_value() != expected_source.wire_value() {
return false;
}
return match data {
std::option::Option::Some(value) => data_matches(value, &metadata.data),
std::option::Option::None => true,
};
}
fn authority_postcondition(
new_authority: std::option::Option<&ks_lib::MdPubkey>,
after: std::option::Option<&crate::SolanaProgramMetadataStatefulReadResult>,
) -> bool {
let observed = match after.map(|value| return &value.state) {
std::option::Option::Some(crate::SolanaProgramMetadataObservedAccountState::Buffer(
value,
)) => value.authority.as_deref(),
std::option::Option::Some(crate::SolanaProgramMetadataObservedAccountState::Metadata(
value,
)) => value.authority.as_deref(),
_ => return false,
};
return observed == new_authority.map(|value| return value.0.as_str());
}
fn set_data_postcondition(
encoding: ks_lib::ExMetadataSpmEncoding,
compression: ks_lib::ExMetadataSpmCompression,
format: ks_lib::ExMetadataSpmFormat,
source: &ks_lib::ExMetadataSpmSetDataSource,
before: std::option::Option<&crate::SolanaProgramMetadataStatefulReadResult>,
after: std::option::Option<&crate::SolanaProgramMetadataStatefulReadResult>,
before_all: &[crate::SolanaProgramMetadataStatefulReadResult],
) -> bool {
let metadata = match after.map(|value| return &value.state) {
std::option::Option::Some(crate::SolanaProgramMetadataObservedAccountState::Metadata(
value,
)) => value,
_ => return false,
};
if metadata.encoding.wire_value() != encoding.wire_value()
|| metadata.compression.wire_value() != compression.wire_value()
|| metadata.format.wire_value() != format.wire_value()
{
return false;
}
return match source {
ks_lib::ExMetadataSpmSetDataSource::PreserveExisting => {
match before.map(|value| return &value.state) {
std::option::Option::Some(
crate::SolanaProgramMetadataObservedAccountState::Metadata(value),
) => metadata.data == value.data && metadata.data_source == value.data_source,
_ => false,
}
},
ks_lib::ExMetadataSpmSetDataSource::Inline { data } => data_matches(data, &metadata.data),
ks_lib::ExMetadataSpmSetDataSource::Buffer { buffer, data_source } => {
if metadata.data_source.wire_value() != data_source.wire_value() {
return false;
}
let source_snapshot = match find_snapshot(before_all, buffer) {
std::option::Option::Some(value) => value,
std::option::Option::None => return false,
};
let bytes = match &source_snapshot.state {
crate::SolanaProgramMetadataObservedAccountState::Buffer(value) => {
value.data.as_slice()
},
_ => return false,
};
return match &metadata.data {
ks_lib::DcMetadataSpmData::Direct(value) => value.as_slice() == bytes,
ks_lib::DcMetadataSpmData::Url(value) => value.as_bytes() == bytes,
ks_lib::DcMetadataSpmData::External(value) => &value.to_wire_bytes()[..] == bytes,
};
},
};
}
fn trim_postcondition(
before: std::option::Option<&crate::SolanaProgramMetadataStatefulReadResult>,
after: std::option::Option<&crate::SolanaProgramMetadataStatefulReadResult>,
) -> bool {
let before = match before {
std::option::Option::Some(value) => value,
std::option::Option::None => return false,
};
let after = match after {
std::option::Option::Some(value) => value,
std::option::Option::None => return false,
};
let same_kind = before.state.code() == after.state.code();
let size_reduced = match (before.space, after.space) {
(std::option::Option::Some(before_space), std::option::Option::Some(after_space)) => {
after_space <= before_space
},
_ => false,
};
return same_kind && size_reduced;
}
fn allocate_postcondition(
authority: &ks_lib::MdPubkey,
seed: std::option::Option<&[u8]>,
canonical: bool,
after: std::option::Option<&crate::SolanaProgramMetadataStatefulReadResult>,
) -> bool {
let buffer = match after.map(|value| return &value.state) {
std::option::Option::Some(crate::SolanaProgramMetadataObservedAccountState::Buffer(
value,
)) => value,
_ => return false,
};
if buffer.authority.as_deref() != std::option::Option::Some(authority.0.as_str()) {
return false;
}
return match seed {
std::option::Option::Some(value) => {
buffer.canonical == canonical && &buffer.seed.as_bytes()[..] == value
},
std::option::Option::None => !buffer.canonical,
};
}
fn extend_postcondition(
length: u16,
before: std::option::Option<&crate::SolanaProgramMetadataStatefulReadResult>,
after: std::option::Option<&crate::SolanaProgramMetadataStatefulReadResult>,
) -> bool {
let before = match before {
std::option::Option::Some(value) => value,
std::option::Option::None => return false,
};
let after = match after {
std::option::Option::Some(value) => value,
std::option::Option::None => return false,
};
let expected = match before.space.and_then(|value| return value.checked_add(u64::from(length)))
{
std::option::Option::Some(value) => value,
std::option::Option::None => return false,
};
return before.state.code() == after.state.code()
&& after.space == std::option::Option::Some(expected);
}
fn data_matches(
input: &ks_lib::ExMetadataSpmDataInput,
observed: &ks_lib::DcMetadataSpmData,
) -> bool {
return match (input, observed) {
(
ks_lib::ExMetadataSpmDataInput::Direct { bytes },
ks_lib::DcMetadataSpmData::Direct(value),
) => bytes == value,
(ks_lib::ExMetadataSpmDataInput::Url { url }, ks_lib::DcMetadataSpmData::Url(value)) => {
url == value
},
(
ks_lib::ExMetadataSpmDataInput::External { address, offset, length },
ks_lib::DcMetadataSpmData::External(value),
) => {
value.address.to_string().as_str() == address.0.as_str()
&& value.offset == *offset
&& value.length.map(std::num::NonZeroU32::get) == *length
},
_ => false,
};
}
#[cfg(test)]
mod tests {
fn pubkey(byte: u8) -> ks_lib::MdPubkey {
return ks_lib::MdPubkey(solana_pubkey::Pubkey::new_from_array([byte; 32]).to_string());
}
#[test]
fn readiness_requires_exact_simulation_and_resolved_signers() {
let signer = pubkey(1);
let plan = ks_lib::ExApiPreparedExecutionPlan {
executor_name: "kb-lib.executor.metadata.solana_program_metadata".to_string(),
executor_version: "0.4.8".to_string(),
intent_id: "spm-readiness".to_string(),
operation_code: ks_lib::EX_METADATA_SPM_WRITE_OPERATION.to_string(),
fee_payer: signer.clone(),
instructions: vec![ks_lib::ExApiPlannedInstruction {
program_id: ks_lib::MdProgramId(
ks_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID.to_string(),
),
operation_code: ks_lib::EX_METADATA_SPM_WRITE_OPERATION.to_string(),
accounts: vec![],
data: vec![0],
}],
required_signers: vec![ks_lib::ExApiRequiredSigner {
pubkey: signer.clone(),
role: "authority".to_string(),
}],
policy: ks_lib::ExApiExecutionPolicy {
dry_run: false,
..ks_lib::ExApiExecutionPolicy::default()
},
requested_spend_lamports: 0,
requested_compute_unit_price_micro_lamports: std::option::Option::None,
};
let request = crate::SolanaProgramMetadataExecutionReadinessRequest {
plan,
preflight: crate::SolanaProgramMetadataPreflightReport {
operation_code: ks_lib::EX_METADATA_SPM_WRITE_OPERATION.to_string(),
target_account: pubkey(2),
context_slot: 99,
inspected_accounts: vec![],
safety: ks_lib::ExSafetyEvaluation {
decision: ks_lib::ExSafetyDecision::Allow,
violations: vec![],
},
runtime_authority_validation_required: false,
checks: vec![],
},
message_hash: "message".to_string(),
simulated_message_hash: "message".to_string(),
simulated: true,
simulation_succeeded: true,
resolved_signers: vec![signer],
submit: true,
operator_confirmed: true,
};
let report = crate::validate_solana_program_metadata_execution_readiness(&request);
assert!(report.is_ok());
if let std::result::Result::Ok(value) = report {
assert!(value.send_authorized);
assert_eq!(value.context_slot, 99);
}
}
#[test]
fn pipeline_matrix_is_machine_readable_and_matches_nine_operations() {
let matrix_result = serde_json::from_str::<serde_json::Value>(include_str!(
"../../test-fixtures/contract-matrices/METADATA_SOLANA_PROGRAM_PIPELINE_MATRIX.json"
));
assert!(matrix_result.is_ok());
if let std::result::Result::Ok(matrix) = matrix_result {
assert_eq!(
matrix["programId"],
ks_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID
);
assert_eq!(
matrix["operations"].as_array().map(std::vec::Vec::len),
std::option::Option::Some(ks_lib::EX_METADATA_SPM_SUPPORTED_OPERATION_CODES.len())
);
assert_eq!(matrix["pipelineCrate"], "ks-pipeline");
assert_eq!(matrix["demoScenarioCrate"], "ks-pipeline-demo-scenarios");
}
}
#[test]
fn every_stable_operation_has_one_explicit_postcondition_path() {
let account = pubkey(4);
let authority = pubkey(5);
let program = pubkey(6);
let destination = pubkey(7);
let operations = vec![
ks_lib::ExMetadataSpmOperation::Write {
buffer: account.clone(),
authority: authority.clone(),
offset: 0,
source: ks_lib::ExMetadataSpmWriteSource::Inline { data: vec![1] },
},
ks_lib::ExMetadataSpmOperation::Initialize {
metadata: account.clone(),
authority: authority.clone(),
program: program.clone(),
program_data: std::option::Option::None,
canonical: false,
seed: vec![0_u8; ks_lib::DC_METADATA_SPM_SEED_BYTES],
encoding: ks_lib::ExMetadataSpmEncoding::Utf8,
compression: ks_lib::ExMetadataSpmCompression::None,
format: ks_lib::ExMetadataSpmFormat::Json,
data_source: ks_lib::ExMetadataSpmDataSource::Direct,
data: std::option::Option::Some(ks_lib::ExMetadataSpmDataInput::Direct {
bytes: vec![1],
}),
allocate_account: true,
},
ks_lib::ExMetadataSpmOperation::SetAuthority {
account: account.clone(),
authority: authority.clone(),
new_authority: std::option::Option::Some(pubkey(8)),
program_context: std::option::Option::None,
},
ks_lib::ExMetadataSpmOperation::SetData {
metadata: account.clone(),
authority: authority.clone(),
encoding: ks_lib::ExMetadataSpmEncoding::Utf8,
compression: ks_lib::ExMetadataSpmCompression::None,
format: ks_lib::ExMetadataSpmFormat::Json,
source: ks_lib::ExMetadataSpmSetDataSource::PreserveExisting,
program_context: std::option::Option::None,
},
ks_lib::ExMetadataSpmOperation::SetImmutable {
metadata: account.clone(),
authority: authority.clone(),
program_context: std::option::Option::None,
},
ks_lib::ExMetadataSpmOperation::Trim {
account: account.clone(),
authority: authority.clone(),
destination: destination.clone(),
program_context: std::option::Option::None,
},
ks_lib::ExMetadataSpmOperation::Close {
account: account.clone(),
authority: authority.clone(),
destination,
program_context: std::option::Option::None,
},
ks_lib::ExMetadataSpmOperation::Allocate {
buffer: account.clone(),
authority: authority.clone(),
seed: std::option::Option::None,
program_context: std::option::Option::None,
canonical: false,
allocate_account: true,
},
ks_lib::ExMetadataSpmOperation::Extend {
account,
authority,
length: 1,
program_context: std::option::Option::None,
},
];
let mut codes = operations
.iter()
.map(|operation| return operation.operation_code())
.collect::<std::vec::Vec<_>>();
codes.sort();
let mut expected = ks_lib::EX_METADATA_SPM_SUPPORTED_OPERATION_CODES.to_vec();
expected.sort();
assert_eq!(codes, expected);
for operation in operations {
let report = crate::inspect_solana_program_metadata_post_execution(
&crate::SolanaProgramMetadataPostExecutionRequest {
operation,
before: vec![],
after: vec![],
},
);
assert!(report.is_ok());
if let std::result::Result::Ok(value) = report {
assert_eq!(
value.status,
crate::SolanaProgramMetadataPostconditionStatus::NotApplicable
);
assert_eq!(value.postconditions.len(), 1);
}
}
}
#[test]
fn postcondition_summary_never_invents_success() {
let account = pubkey(3);
let item = crate::SolanaProgramMetadataPostcondition {
code: "spm.none".to_string(),
account,
status: crate::SolanaProgramMetadataPostconditionStatus::NotApplicable,
diagnostic: "missing after state".to_string(),
};
assert_eq!(
crate::summarize_solana_program_metadata_postconditions(&[item]),
crate::SolanaProgramMetadataPostconditionStatus::NotApplicable
);
}
}

View File

@@ -0,0 +1,761 @@
// file: ks-pipeline/src/metadata_solana_program_preflight.rs
// version: 3
//! Stateful preflight for Solana Program Metadata execution plans.
/// Maximum number of bounded account snapshots accepted by one preflight.
pub const MAX_SOLANA_PROGRAM_METADATA_PREFLIGHT_ACCOUNTS: usize = 16;
/// One rent-exemption observation supplied by the caller for a growth operation.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct SolanaProgramMetadataRentObservation {
/// Account whose allocation or growth is being validated.
pub account: ks_lib::MdPubkey,
/// Target account-data size used for the rent query.
pub target_space: u64,
/// Lamports observed on the account before execution.
pub observed_lamports: u64,
/// Minimum lamports returned for `target_space`.
pub required_lamports: u64,
}
impl crate::SolanaProgramMetadataRentObservation {
/// Returns whether the account is sufficiently pre-funded.
pub const fn is_satisfied(&self) -> bool {
return self.observed_lamports >= self.required_lamports;
}
}
/// Complete stateful preflight request for one prepared Program Metadata plan.
#[derive(Clone, Debug, PartialEq)]
pub struct SolanaProgramMetadataPreflightRequest {
/// Exact typed intent used to build the plan.
pub intent: ks_lib::ExMetadataSpmExecutionIntent,
/// Exact prepared plan produced by `ks-lib`.
pub plan: ks_lib::ExApiPreparedExecutionPlan,
/// Confirmed account snapshots observed before simulation.
pub before: std::vec::Vec<crate::SolanaProgramMetadataStatefulReadResult>,
/// Explicit rent observations required by account growth operations.
pub rent_observations: std::vec::Vec<crate::SolanaProgramMetadataRentObservation>,
}
/// Deterministic Program Metadata preflight report.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct SolanaProgramMetadataPreflightReport {
/// Stable operation code.
pub operation_code: std::string::String,
/// Primary account mutated by the instruction.
pub target_account: ks_lib::MdPubkey,
/// Highest confirmed context slot across supplied snapshots.
pub context_slot: u64,
/// Canonical accounts inspected by the preflight.
pub inspected_accounts: std::vec::Vec<ks_lib::MdPubkey>,
/// Safety decision applied before simulation.
pub safety: ks_lib::ExSafetyEvaluation,
/// Whether the on-chain program must validate an upgrade authority at runtime.
pub runtime_authority_validation_required: bool,
/// Ordered successful checks.
pub checks: std::vec::Vec<std::string::String>,
}
/// Validates one Program Metadata plan against confirmed pre-execution state.
pub fn inspect_solana_program_metadata_preflight(
request: &crate::SolanaProgramMetadataPreflightRequest,
) -> ks_core::Result<crate::SolanaProgramMetadataPreflightReport> {
if request.before.len() > crate::MAX_SOLANA_PROGRAM_METADATA_PREFLIGHT_ACCOUNTS {
return std::result::Result::Err(ks_core::Error::new(
"metadata_solana_program_preflight_account_limit_exceeded",
"Solana Program Metadata preflight account limit exceeded",
));
}
let operation_code = request.intent.operation.operation_code();
if request.plan.operation_code != operation_code {
return std::result::Result::Err(ks_core::Error::new(
"metadata_solana_program_preflight_operation_mismatch",
"Program Metadata intent and plan operation codes must match",
));
}
if request.plan.instructions.len() != 1 {
return std::result::Result::Err(ks_core::Error::new(
"metadata_solana_program_preflight_instruction_count_invalid",
"Program Metadata plans must contain exactly one instruction",
));
}
let instruction = match request.plan.instructions.first() {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::new(
"metadata_solana_program_preflight_instruction_missing",
"Program Metadata plan instruction is missing",
));
},
};
if instruction.program_id.0 != ks_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID
|| instruction.operation_code != operation_code
|| !ks_lib::EX_METADATA_SPM_SUPPORTED_OPERATION_CODES.contains(&operation_code)
{
return std::result::Result::Err(ks_core::Error::new(
"metadata_solana_program_preflight_instruction_contract_mismatch",
"Program Metadata plan does not preserve the exact program and operation contract",
));
}
if request.intent.operation.requires_explicit_approval()
&& !request.intent.allow_destructive_operation
{
return std::result::Result::Err(ks_core::Error::new(
"metadata_solana_program_preflight_destructive_approval_required",
"destructive Program Metadata execution requires explicit approval",
));
}
let safety = match ks_lib::ExSafetyChecker.evaluate_prepared_plan(&request.plan) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if safety.decision == ks_lib::ExSafetyDecision::Deny {
return std::result::Result::Err(ks_core::Error::new(
"metadata_solana_program_preflight_safety_denied",
safety_violation_codes(safety.violations.as_slice()),
));
}
let mut snapshots = std::collections::BTreeMap::<
std::string::String,
&crate::SolanaProgramMetadataStatefulReadResult,
>::new();
let mut context_slot = 0_u64;
for snapshot in &request.before {
if snapshot.commitment != "confirmed" {
return std::result::Result::Err(ks_core::Error::new(
"metadata_solana_program_preflight_commitment_mismatch",
"Program Metadata stateful snapshots must use confirmed commitment",
));
}
if snapshots.insert(snapshot.account.0.clone(), snapshot).is_some() {
return std::result::Result::Err(ks_core::Error::new(
"metadata_solana_program_preflight_duplicate_account",
format!("duplicate Program Metadata snapshot for {}", snapshot.account.0),
));
}
context_slot = context_slot.max(snapshot.context_slot);
}
let target_account = operation_target(&request.intent.operation).clone();
let target = match snapshots.get(target_account.0.as_str()) {
std::option::Option::Some(value) => *value,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::new(
"metadata_solana_program_preflight_target_snapshot_missing",
format!("missing preflight snapshot for {}", target_account.0),
));
},
};
if let std::result::Result::Err(error) =
validate_target_state(&request.intent.operation, target)
{
return std::result::Result::Err(error);
}
if let std::result::Result::Err(error) =
validate_source_state(&request.intent.operation, &snapshots, target)
{
return std::result::Result::Err(error);
}
let runtime_authority_validation_required =
match validate_authority_state(&request.intent.operation, target) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if let std::result::Result::Err(error) = validate_rent_observations(
&request.intent.operation,
target,
&snapshots,
request.rent_observations.as_slice(),
) {
return std::result::Result::Err(error);
}
let inspected_accounts = snapshots
.values()
.map(|value| return value.account.clone())
.collect::<std::vec::Vec<_>>();
tracing::debug!(
target: crate::TRACING_TARGET,
action = "inspect_solana_program_metadata_preflight",
operation_code,
target_account = %target_account.0,
context_slot,
inspected_account_count = inspected_accounts.len(),
runtime_authority_validation_required,
safety_decision = ?safety.decision,
"validated Solana Program Metadata stateful preflight"
);
return std::result::Result::Ok(crate::SolanaProgramMetadataPreflightReport {
operation_code: operation_code.to_string(),
target_account,
context_slot,
inspected_accounts,
safety,
runtime_authority_validation_required,
checks: vec![
"program_id_exact".to_string(),
"operation_code_exact".to_string(),
"single_instruction_plan".to_string(),
"confirmed_state_snapshots".to_string(),
"target_state_compatible".to_string(),
"source_state_compatible".to_string(),
"authority_contract_checked".to_string(),
"rent_prefunding_checked_when_required".to_string(),
"common_safety_checked_before_simulation".to_string(),
],
});
}
fn operation_target(operation: &ks_lib::ExMetadataSpmOperation) -> &ks_lib::MdPubkey {
return match operation {
ks_lib::ExMetadataSpmOperation::Write { buffer, .. }
| ks_lib::ExMetadataSpmOperation::Allocate { buffer, .. } => buffer,
ks_lib::ExMetadataSpmOperation::Initialize { metadata, .. }
| ks_lib::ExMetadataSpmOperation::SetData { metadata, .. }
| ks_lib::ExMetadataSpmOperation::SetImmutable { metadata, .. } => metadata,
ks_lib::ExMetadataSpmOperation::SetAuthority { account, .. }
| ks_lib::ExMetadataSpmOperation::Trim { account, .. }
| ks_lib::ExMetadataSpmOperation::Close { account, .. }
| ks_lib::ExMetadataSpmOperation::Extend { account, .. } => account,
};
}
fn validate_target_state(
operation: &ks_lib::ExMetadataSpmOperation,
target: &crate::SolanaProgramMetadataStatefulReadResult,
) -> ks_core::Result<()> {
let valid = match operation {
ks_lib::ExMetadataSpmOperation::Write { .. } => {
matches!(&target.state, crate::SolanaProgramMetadataObservedAccountState::Buffer(_))
},
ks_lib::ExMetadataSpmOperation::Initialize { data, .. } => {
if data.is_some() {
is_prefunded_uninitialized(target)
} else {
matches!(&target.state, crate::SolanaProgramMetadataObservedAccountState::Buffer(_))
}
},
ks_lib::ExMetadataSpmOperation::SetData { .. }
| ks_lib::ExMetadataSpmOperation::SetImmutable { .. } => {
matches!(&target.state, crate::SolanaProgramMetadataObservedAccountState::Metadata(_))
},
ks_lib::ExMetadataSpmOperation::Allocate { .. } => is_prefunded_uninitialized(target),
ks_lib::ExMetadataSpmOperation::SetAuthority { .. }
| ks_lib::ExMetadataSpmOperation::Trim { .. }
| ks_lib::ExMetadataSpmOperation::Close { .. }
| ks_lib::ExMetadataSpmOperation::Extend { .. } => matches!(
&target.state,
crate::SolanaProgramMetadataObservedAccountState::Buffer(_)
| crate::SolanaProgramMetadataObservedAccountState::Metadata(_)
),
};
if !valid {
return std::result::Result::Err(ks_core::Error::new(
"metadata_solana_program_preflight_target_state_invalid",
format!(
"operation {} cannot use target state {}",
operation.operation_code(),
target.state.code()
),
));
}
return std::result::Result::Ok(());
}
fn is_prefunded_uninitialized(target: &crate::SolanaProgramMetadataStatefulReadResult) -> bool {
return match &target.state {
crate::SolanaProgramMetadataObservedAccountState::Uninitialized {
owner,
lamports,
space: _,
} => {
*lamports > 0
&& (owner.0 == ks_program_ids::SYSTEM_PROGRAM_ID
|| owner.0 == ks_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID)
},
_ => false,
};
}
fn validate_source_state(
operation: &ks_lib::ExMetadataSpmOperation,
snapshots: &std::collections::BTreeMap<
std::string::String,
&crate::SolanaProgramMetadataStatefulReadResult,
>,
target: &crate::SolanaProgramMetadataStatefulReadResult,
) -> ks_core::Result<()> {
match operation {
ks_lib::ExMetadataSpmOperation::Write { offset, source, .. } => {
let target_buffer = match &target.state {
crate::SolanaProgramMetadataObservedAccountState::Buffer(value) => value,
_ => return std::result::Result::Ok(()),
};
let source_length = match source {
ks_lib::ExMetadataSpmWriteSource::Inline { data } => data.len(),
ks_lib::ExMetadataSpmWriteSource::Buffer { source_buffer } => {
let source_snapshot = match snapshots.get(source_buffer.0.as_str()) {
std::option::Option::Some(value) => *value,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::new(
"metadata_solana_program_preflight_source_buffer_missing",
format!("missing source Buffer snapshot for {}", source_buffer.0),
));
},
};
match &source_snapshot.state {
crate::SolanaProgramMetadataObservedAccountState::Buffer(value) => {
value.data.len()
},
_ => {
return std::result::Result::Err(ks_core::Error::new(
"metadata_solana_program_preflight_source_buffer_invalid",
"Write source account must be an initialized Buffer",
));
},
}
},
};
let offset = match usize::try_from(*offset) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"metadata_solana_program_preflight_write_offset_invalid",
error.to_string(),
));
},
};
if offset.saturating_add(source_length) > target_buffer.allocated_data_bytes {
return std::result::Result::Err(ks_core::Error::new(
"metadata_solana_program_preflight_write_out_of_bounds",
"Write source exceeds the allocated Buffer capacity",
));
}
},
ks_lib::ExMetadataSpmOperation::SetData {
source: ks_lib::ExMetadataSpmSetDataSource::Buffer { buffer, .. },
..
} => {
let source_snapshot = match snapshots.get(buffer.0.as_str()) {
std::option::Option::Some(value) => *value,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::new(
"metadata_solana_program_preflight_source_buffer_missing",
format!("missing source Buffer snapshot for {}", buffer.0),
));
},
};
if !matches!(
&source_snapshot.state,
crate::SolanaProgramMetadataObservedAccountState::Buffer(_)
) {
return std::result::Result::Err(ks_core::Error::new(
"metadata_solana_program_preflight_source_buffer_invalid",
"SetData source account must be an initialized Buffer",
));
}
},
_ => {},
}
return std::result::Result::Ok(());
}
fn validate_authority_state(
operation: &ks_lib::ExMetadataSpmOperation,
target: &crate::SolanaProgramMetadataStatefulReadResult,
) -> ks_core::Result<bool> {
let (authority, has_program_context) = match operation {
ks_lib::ExMetadataSpmOperation::Write { authority: _, .. } => {
return std::result::Result::Ok(false);
},
ks_lib::ExMetadataSpmOperation::Initialize { canonical, .. } => {
return std::result::Result::Ok(*canonical);
},
ks_lib::ExMetadataSpmOperation::Allocate { seed, canonical, .. } => {
return std::result::Result::Ok(seed.is_some() && *canonical);
},
ks_lib::ExMetadataSpmOperation::SetAuthority { authority, program_context, .. }
| ks_lib::ExMetadataSpmOperation::SetData { authority, program_context, .. }
| ks_lib::ExMetadataSpmOperation::SetImmutable { authority, program_context, .. }
| ks_lib::ExMetadataSpmOperation::Trim { authority, program_context, .. }
| ks_lib::ExMetadataSpmOperation::Close { authority, program_context, .. }
| ks_lib::ExMetadataSpmOperation::Extend { authority, program_context, .. } => {
(authority, program_context.is_some())
},
};
let (current_authority, canonical) = match &target.state {
crate::SolanaProgramMetadataObservedAccountState::Buffer(value) => {
(value.authority.as_deref(), value.canonical)
},
crate::SolanaProgramMetadataObservedAccountState::Metadata(value) => {
(value.authority.as_deref(), value.canonical)
},
_ => return std::result::Result::Ok(false),
};
if current_authority == std::option::Option::Some(authority.0.as_str()) {
return std::result::Result::Ok(false);
}
if canonical && has_program_context {
return std::result::Result::Ok(true);
}
return std::result::Result::Err(ks_core::Error::new(
"metadata_solana_program_preflight_authority_mismatch",
"the supplied authority does not match account state and no canonical program context was provided",
));
}
fn validate_rent_observations(
operation: &ks_lib::ExMetadataSpmOperation,
target: &crate::SolanaProgramMetadataStatefulReadResult,
snapshots: &std::collections::BTreeMap<
std::string::String,
&crate::SolanaProgramMetadataStatefulReadResult,
>,
observations: &[crate::SolanaProgramMetadataRentObservation],
) -> ks_core::Result<()> {
let expected_space = match expected_rent_target_space(operation, target, snapshots) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let expected_space = match expected_space {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Ok(()),
};
let observation = match observations.iter().find(|value| return value.account == target.account)
{
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::new(
"metadata_solana_program_preflight_rent_observation_missing",
format!("missing rent observation for {}", target.account.0),
));
},
};
let observed_lamports = match target.lamports {
std::option::Option::Some(value) => value,
std::option::Option::None => 0,
};
if observation.target_space != expected_space
|| observation.observed_lamports != observed_lamports
|| !observation.is_satisfied()
{
return std::result::Result::Err(ks_core::Error::new(
"metadata_solana_program_preflight_rent_not_satisfied",
format!(
"account {} has {} lamports but requires {} for exact target space {}",
observation.account.0,
observation.observed_lamports,
observation.required_lamports,
expected_space
),
));
}
return std::result::Result::Ok(());
}
fn expected_rent_target_space(
operation: &ks_lib::ExMetadataSpmOperation,
target: &crate::SolanaProgramMetadataStatefulReadResult,
snapshots: &std::collections::BTreeMap<
std::string::String,
&crate::SolanaProgramMetadataStatefulReadResult,
>,
) -> ks_core::Result<std::option::Option<u64>> {
let header = match u64::try_from(ks_lib::DC_METADATA_SPM_METADATA_HEADER_BYTES) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"metadata_solana_program_preflight_header_size_overflow",
error.to_string(),
));
},
};
return match operation {
ks_lib::ExMetadataSpmOperation::Initialize {
data: std::option::Option::Some(data),
..
} => add_metadata_header(header, metadata_input_wire_len(data)),
ks_lib::ExMetadataSpmOperation::SetData {
source: ks_lib::ExMetadataSpmSetDataSource::PreserveExisting,
..
} => std::result::Result::Ok(std::option::Option::None),
ks_lib::ExMetadataSpmOperation::SetData {
source: ks_lib::ExMetadataSpmSetDataSource::Inline { data },
..
} => add_metadata_header(header, metadata_input_wire_len(data)),
ks_lib::ExMetadataSpmOperation::SetData {
source: ks_lib::ExMetadataSpmSetDataSource::Buffer { buffer, .. },
..
} => {
let source = match snapshots.get(buffer.0.as_str()) {
std::option::Option::Some(value) => *value,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::new(
"metadata_solana_program_preflight_source_buffer_missing",
format!("missing source Buffer snapshot for {}", buffer.0),
));
},
};
let length = match &source.state {
crate::SolanaProgramMetadataObservedAccountState::Buffer(value) => {
match u64::try_from(value.data.len()) {
std::result::Result::Ok(length) => length,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"metadata_solana_program_preflight_source_length_overflow",
error.to_string(),
));
},
}
},
_ => {
return std::result::Result::Err(ks_core::Error::new(
"metadata_solana_program_preflight_source_buffer_invalid",
"SetData source account must be an initialized Buffer",
));
},
};
return add_metadata_header(header, std::result::Result::Ok(length));
},
ks_lib::ExMetadataSpmOperation::Allocate { .. } => {
let current = match target.space {
std::option::Option::Some(value) => value,
std::option::Option::None => 0,
};
std::result::Result::Ok(std::option::Option::Some(current.max(header)))
},
ks_lib::ExMetadataSpmOperation::Extend { length, .. } => {
let current = match target.space {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::new(
"metadata_solana_program_preflight_target_space_missing",
"Extend requires the current account space",
));
},
};
return match current.checked_add(u64::from(*length)) {
std::option::Option::Some(value) => {
std::result::Result::Ok(std::option::Option::Some(value))
},
std::option::Option::None => std::result::Result::Err(ks_core::Error::new(
"metadata_solana_program_preflight_target_space_overflow",
"Extend target space overflowed u64",
)),
};
},
_ => std::result::Result::Ok(std::option::Option::None),
};
}
fn metadata_input_wire_len(data: &ks_lib::ExMetadataSpmDataInput) -> ks_core::Result<u64> {
let length = match data {
ks_lib::ExMetadataSpmDataInput::Direct { bytes } => bytes.len(),
ks_lib::ExMetadataSpmDataInput::Url { url } => url.len(),
ks_lib::ExMetadataSpmDataInput::External { .. } => {
ks_lib::DC_METADATA_SPM_EXTERNAL_DATA_BYTES
},
};
return match u64::try_from(length) {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(ks_core::Error::new(
"metadata_solana_program_preflight_data_length_overflow",
error.to_string(),
)),
};
}
fn add_metadata_header(
header: u64,
data_length: ks_core::Result<u64>,
) -> ks_core::Result<std::option::Option<u64>> {
let data_length = match data_length {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return match header.checked_add(data_length) {
std::option::Option::Some(value) => {
std::result::Result::Ok(std::option::Option::Some(value))
},
std::option::Option::None => std::result::Result::Err(ks_core::Error::new(
"metadata_solana_program_preflight_target_space_overflow",
"Program Metadata target space overflowed u64",
)),
};
}
fn safety_violation_codes(violations: &[ks_lib::ExSafetyViolation]) -> std::string::String {
if violations.is_empty() {
return "Program Metadata execution safety denied without a diagnostic".to_string();
}
return violations
.iter()
.map(|value| return value.code.as_str())
.collect::<std::vec::Vec<_>>()
.join(",");
}
#[cfg(test)]
mod tests {
fn pubkey(value: u8) -> ks_lib::MdPubkey {
return ks_lib::MdPubkey(solana_pubkey::Pubkey::new_from_array([value; 32]).to_string());
}
fn policy(authority: &ks_lib::MdPubkey) -> ks_lib::ExApiExecutionPolicy {
return ks_lib::ExApiExecutionPolicy {
cost_limit: ks_lib::ExApiExecutionCostLimit {
max_spend_lamports: std::option::Option::Some(0),
max_fee_lamports: std::option::Option::Some(20_000),
max_compute_unit_price_micro_lamports: std::option::Option::None,
},
authorized_signers: vec![authority.clone()],
post_execution_validation: ks_lib::ExApiPostExecutionValidationPolicy {
canonical_insert_required: true,
core_extraction_required: true,
decode_replay_required: true,
materialization_required: true,
},
..ks_lib::ExApiExecutionPolicy::default()
};
}
fn buffer_read(
account: ks_lib::MdPubkey,
authority: ks_lib::MdPubkey,
) -> crate::SolanaProgramMetadataStatefulReadResult {
return crate::SolanaProgramMetadataStatefulReadResult {
commitment: "confirmed".to_string(),
context_slot: 42,
account: account.clone(),
lamports: std::option::Option::Some(1_000),
owner: std::option::Option::Some(ks_lib::MdProgramId(
ks_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID.to_string(),
)),
space: std::option::Option::Some(100),
state: crate::SolanaProgramMetadataObservedAccountState::Buffer(
ks_lib::DcMetadataSpmBufferAccountSnapshot {
account: account.0,
program: std::option::Option::None,
authority: std::option::Option::Some(authority.0),
canonical: false,
seed: ks_lib::DcMetadataSpmSeed::default(),
data: vec![0_u8; 4],
allocated_data_bytes: 4,
},
),
materialized_output: std::option::Option::None,
};
}
#[test]
fn write_preflight_requires_buffer_capacity_and_common_safety() {
let authority = pubkey(2);
let buffer = pubkey(3);
let intent = ks_lib::ExMetadataSpmExecutionIntent {
intent_id: "spm-preflight-write".to_string(),
fee_payer: authority.clone(),
policy: policy(&authority),
allow_destructive_operation: true,
operation: ks_lib::ExMetadataSpmOperation::Write {
buffer: buffer.clone(),
authority: authority.clone(),
offset: 2,
source: ks_lib::ExMetadataSpmWriteSource::Inline { data: vec![8, 9] },
},
};
let plan = ks_lib::ExApiTypedInstructionExecutor::build_prepared_plan(
&ks_lib::ExMetadataSolanaProgramMetadataExecutor,
&intent,
);
assert!(plan.is_ok());
let plan = match plan {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let report = crate::inspect_solana_program_metadata_preflight(
&crate::SolanaProgramMetadataPreflightRequest {
intent,
plan,
before: vec![buffer_read(buffer, authority)],
rent_observations: vec![],
},
);
assert!(report.is_ok());
if let std::result::Result::Ok(value) = report {
assert_eq!(value.operation_code, ks_lib::EX_METADATA_SPM_WRITE_OPERATION);
assert_eq!(value.safety.decision, ks_lib::ExSafetyDecision::Allow);
}
}
#[test]
fn allocate_requires_vacancy_and_explicit_rent_evidence() {
let authority = pubkey(4);
let buffer = authority.clone();
let intent = ks_lib::ExMetadataSpmExecutionIntent {
intent_id: "spm-preflight-allocate".to_string(),
fee_payer: authority.clone(),
policy: policy(&authority),
allow_destructive_operation: false,
operation: ks_lib::ExMetadataSpmOperation::Allocate {
buffer: buffer.clone(),
authority: authority.clone(),
seed: std::option::Option::None,
program_context: std::option::Option::None,
canonical: false,
allocate_account: true,
},
};
let plan = ks_lib::ExApiTypedInstructionExecutor::build_prepared_plan(
&ks_lib::ExMetadataSolanaProgramMetadataExecutor,
&intent,
);
assert!(plan.is_ok());
let plan = match plan {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let before = crate::SolanaProgramMetadataStatefulReadResult {
commitment: "confirmed".to_string(),
context_slot: 9,
account: buffer.clone(),
lamports: std::option::Option::Some(1_000),
owner: std::option::Option::Some(ks_lib::MdProgramId(
ks_program_ids::SYSTEM_PROGRAM_ID.to_string(),
)),
space: std::option::Option::Some(0),
state: crate::SolanaProgramMetadataObservedAccountState::Uninitialized {
owner: ks_lib::MdProgramId(ks_program_ids::SYSTEM_PROGRAM_ID.to_string()),
lamports: 1_000,
space: 0,
},
materialized_output: std::option::Option::None,
};
let missing_rent = crate::inspect_solana_program_metadata_preflight(
&crate::SolanaProgramMetadataPreflightRequest {
intent: intent.clone(),
plan: plan.clone(),
before: vec![before.clone()],
rent_observations: vec![],
},
);
assert!(missing_rent.is_err());
let target_space = match u64::try_from(ks_lib::DC_METADATA_SPM_BUFFER_HEADER_BYTES) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let report = crate::inspect_solana_program_metadata_preflight(
&crate::SolanaProgramMetadataPreflightRequest {
intent,
plan,
before: vec![before],
rent_observations: vec![crate::SolanaProgramMetadataRentObservation {
account: buffer,
target_space,
observed_lamports: 1_000,
required_lamports: 900,
}],
},
);
assert!(report.is_ok());
}
}

View File

@@ -0,0 +1,442 @@
// file: ks-pipeline/src/metadata_solana_program_stateful.rs
// version: 5
//! Bounded Solana Program Metadata account reads and authoritative state projections.
/// Maximum complete Solana Program Metadata account data accepted by one RPC read.
///
/// The decoder can validate larger account byte arrays supplied by offline or chunked sources, but
/// the current execution RPC adapter intentionally bounds one complete `getAccountInfo` response.
pub const MAX_SOLANA_PROGRAM_METADATA_STATEFUL_ACCOUNT_BYTES: usize =
ks_onchain_transport::MAX_COMPLETE_ACCOUNT_DATA_BYTES;
/// Expected state of one Solana Program Metadata account read.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SolanaProgramMetadataExpectedAccountState {
/// Accept every supported state.
Any,
/// Accept an absent or zeroed account before initialization.
Vacant,
/// Require an absent account.
Missing,
/// Require an initialized Buffer account.
Buffer,
/// Require an initialized Metadata account.
Metadata,
}
/// Supported state observed for one Solana Program Metadata account.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(tag = "state", content = "snapshot", rename_all = "snake_case")]
pub enum SolanaProgramMetadataObservedAccountState {
/// The address does not currently exist.
Missing,
/// The address exists but contains only zeroed or empty data and is not initialized.
Uninitialized {
/// Current owner before Program Metadata assignment.
owner: ks_lib::MdProgramId,
/// Current lamports retained by the account.
lamports: u64,
/// Current allocated account-data bytes.
space: u64,
},
/// Decoded Program Metadata Buffer account.
Buffer(ks_lib::DcMetadataSpmBufferAccountSnapshot),
/// Decoded Program Metadata Metadata account.
Metadata(ks_lib::DcMetadataSpmMetadataAccountSnapshot),
}
impl crate::SolanaProgramMetadataObservedAccountState {
/// Returns the stable lowercase state code.
pub const fn code(&self) -> &'static str {
return match self {
Self::Missing => "missing",
Self::Uninitialized { owner: _, lamports: _, space: _ } => "uninitialized",
Self::Buffer(_) => "buffer",
Self::Metadata(_) => "metadata",
};
}
/// Returns whether the state is absent or uninitialized.
pub const fn is_vacant(&self) -> bool {
return matches!(
self,
Self::Missing | Self::Uninitialized { owner: _, lamports: _, space: _ }
);
}
}
/// One bounded Solana Program Metadata account read request.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct SolanaProgramMetadataStatefulReadRequest {
/// Endpoint role used for the HTTP RPC request.
pub query_role: std::string::String,
/// Account address to inspect.
pub account: ks_lib::MdPubkey,
/// Expected account state.
pub expected_state: crate::SolanaProgramMetadataExpectedAccountState,
/// 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,
}
impl crate::SolanaProgramMetadataStatefulReadRequest {
/// Validates the bounded request.
pub fn validate(&self) -> ks_core::Result<()> {
if self.query_role.trim().is_empty() {
return std::result::Result::Err(ks_core::Error::config(
"Solana Program Metadata stateful query_role must not be empty",
));
}
let account_result = ks_onchain_transport::validate_solana_pubkey_text(
self.account.0.as_str(),
"Solana Program Metadata stateful account",
);
if let std::result::Result::Err(error) = account_result {
return std::result::Result::Err(error);
}
if self.max_data_bytes == 0
|| self.max_data_bytes > crate::MAX_SOLANA_PROGRAM_METADATA_STATEFUL_ACCOUNT_BYTES
{
return std::result::Result::Err(ks_core::Error::config(format!(
"Solana Program Metadata stateful max_data_bytes must be between 1 and {}",
crate::MAX_SOLANA_PROGRAM_METADATA_STATEFUL_ACCOUNT_BYTES
)));
}
return std::result::Result::Ok(());
}
}
/// One bounded RPC read and its validated Program Metadata state.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct SolanaProgramMetadataStatefulReadResult {
/// Commitment used by the read.
pub commitment: std::string::String,
/// Context slot returned by the endpoint.
pub context_slot: u64,
/// Account address requested by the caller.
pub account: ks_lib::MdPubkey,
/// Current lamports when the account exists.
pub lamports: std::option::Option<u64>,
/// Current owner when the account exists.
pub owner: std::option::Option<ks_lib::MdProgramId>,
/// Current account-data allocation when the account exists.
pub space: std::option::Option<u64>,
/// Validated account state.
pub state: crate::SolanaProgramMetadataObservedAccountState,
/// Authoritative materialized projection for Buffer or Metadata state.
pub materialized_output: std::option::Option<ks_lib::MtApiMaterializedOutput>,
}
/// Reads one bounded Solana Program Metadata account.
pub async fn read_solana_program_metadata_stateful_snapshot(
pool: &ks_onchain_transport::HttpEndpointPool,
request: &crate::SolanaProgramMetadataStatefulReadRequest,
) -> ks_core::Result<crate::SolanaProgramMetadataStatefulReadResult> {
if let std::result::Result::Err(error) = request.validate() {
return std::result::Result::Err(error);
}
let config = match ks_onchain_transport::GetAccountInfoConfig::new_with_data(
ks_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_solana_program_metadata_account_info_result(request, &result);
}
/// Validates one RPC account response, decodes its state and emits the authoritative projection.
pub fn materialize_solana_program_metadata_account_info_result(
request: &crate::SolanaProgramMetadataStatefulReadRequest,
result: &ks_onchain_transport::AccountInfoResult,
) -> ks_core::Result<crate::SolanaProgramMetadataStatefulReadResult> {
if let std::result::Result::Err(error) = request.validate() {
return std::result::Result::Err(error);
}
if let std::option::Option::Some(minimum) = request.min_context_slot {
if result.context.slot < minimum {
return std::result::Result::Err(ks_core::Error::new(
"metadata_solana_program_stateful_context_slot_too_old",
"Solana Program Metadata account context slot is below the requested minimum",
));
}
}
let output = match result.account.as_ref() {
std::option::Option::None => crate::SolanaProgramMetadataStatefulReadResult {
commitment: "confirmed".to_string(),
context_slot: result.context.slot,
account: request.account.clone(),
lamports: std::option::Option::None,
owner: std::option::Option::None,
space: std::option::Option::None,
state: crate::SolanaProgramMetadataObservedAccountState::Missing,
materialized_output: std::option::Option::None,
},
std::option::Option::Some(account) => {
if account.executable {
return std::result::Result::Err(ks_core::Error::new(
"metadata_solana_program_stateful_account_executable",
"Solana Program Metadata state accounts must not be executable",
));
}
let data_len = match u64::try_from(account.data.len()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"metadata_solana_program_stateful_account_length_overflow",
error.to_string(),
));
},
};
if account.space != data_len || account.data.len() > request.max_data_bytes {
return std::result::Result::Err(ks_core::Error::new(
"metadata_solana_program_stateful_account_data_invalid",
"Solana Program Metadata account data is incomplete or above the configured bound",
));
}
let zeroed = account.data.iter().all(|byte| return *byte == 0);
if account.data.is_empty() || zeroed {
crate::SolanaProgramMetadataStatefulReadResult {
commitment: "confirmed".to_string(),
context_slot: result.context.slot,
account: request.account.clone(),
lamports: std::option::Option::Some(account.lamports),
owner: std::option::Option::Some(account.owner.clone()),
space: std::option::Option::Some(account.space),
state: crate::SolanaProgramMetadataObservedAccountState::Uninitialized {
owner: account.owner.clone(),
lamports: account.lamports,
space: account.space,
},
materialized_output: std::option::Option::None,
}
} else {
if account.owner.0 != ks_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID {
return std::result::Result::Err(ks_core::Error::new(
"metadata_solana_program_stateful_owner_mismatch",
"initialized Solana Program Metadata state must be owned by ProgM6",
));
}
let snapshot = match ks_lib::decoder_metadata_solana_program_metadata_decode_account(
request.account.0.as_str(),
account.owner.0.as_str(),
account.data.as_slice(),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"metadata_solana_program_stateful_decode_failed",
error.to_string(),
));
},
};
let materialized = match ks_lib::materializer_metadata_materialize_solana_program_metadata_account_snapshot(
result.context.slot,
&snapshot,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"metadata_solana_program_stateful_materialization_failed",
error,
));
},
};
let state = match snapshot {
ks_lib::DcMetadataSpmAccountSnapshot::Buffer(value) => {
crate::SolanaProgramMetadataObservedAccountState::Buffer(value)
},
ks_lib::DcMetadataSpmAccountSnapshot::Metadata(value) => {
crate::SolanaProgramMetadataObservedAccountState::Metadata(value)
},
};
crate::SolanaProgramMetadataStatefulReadResult {
commitment: "confirmed".to_string(),
context_slot: result.context.slot,
account: request.account.clone(),
lamports: std::option::Option::Some(account.lamports),
owner: std::option::Option::Some(account.owner.clone()),
space: std::option::Option::Some(account.space),
state,
materialized_output: std::option::Option::Some(materialized),
}
}
},
};
if !expected_state_matches(request.expected_state, &output.state) {
return std::result::Result::Err(ks_core::Error::new(
"metadata_solana_program_stateful_expected_state_mismatch",
format!(
"expected {:?}, observed {} for {}",
request.expected_state,
output.state.code(),
request.account.0
),
));
}
tracing::debug!(
target: crate::TRACING_TARGET,
action = "read_solana_program_metadata_state",
account = %request.account.0,
context_slot = result.context.slot,
observed_state = output.state.code(),
materialized = output.materialized_output.is_some(),
"validated Solana Program Metadata account state"
);
return std::result::Result::Ok(output);
}
fn expected_state_matches(
expected: crate::SolanaProgramMetadataExpectedAccountState,
observed: &crate::SolanaProgramMetadataObservedAccountState,
) -> bool {
return match expected {
crate::SolanaProgramMetadataExpectedAccountState::Any => true,
crate::SolanaProgramMetadataExpectedAccountState::Vacant => observed.is_vacant(),
crate::SolanaProgramMetadataExpectedAccountState::Missing => {
matches!(observed, crate::SolanaProgramMetadataObservedAccountState::Missing)
},
crate::SolanaProgramMetadataExpectedAccountState::Buffer => {
matches!(observed, crate::SolanaProgramMetadataObservedAccountState::Buffer(_))
},
crate::SolanaProgramMetadataExpectedAccountState::Metadata => {
matches!(observed, crate::SolanaProgramMetadataObservedAccountState::Metadata(_))
},
};
}
#[cfg(test)]
mod tests {
fn request(
expected_state: crate::SolanaProgramMetadataExpectedAccountState,
) -> crate::SolanaProgramMetadataStatefulReadRequest {
return crate::SolanaProgramMetadataStatefulReadRequest {
query_role: "http_queries".to_string(),
account: ks_lib::MdPubkey(
solana_pubkey::Pubkey::new_from_array([3_u8; 32]).to_string(),
),
expected_state,
min_context_slot: std::option::Option::None,
max_data_bytes: 1024,
};
}
#[test]
fn missing_and_zeroed_accounts_are_distinguished_and_bounded() {
let missing = ks_onchain_transport::AccountInfoResult {
context: ks_onchain_transport::RpcResponseContext {
slot: 10,
api_version: std::option::Option::None,
},
account: std::option::Option::None,
};
let missing_result = crate::materialize_solana_program_metadata_account_info_result(
&request(crate::SolanaProgramMetadataExpectedAccountState::Vacant),
&missing,
);
assert!(matches!(
missing_result.as_ref().map(|value| return &value.state),
std::result::Result::Ok(crate::SolanaProgramMetadataObservedAccountState::Missing)
));
let zeroed = ks_onchain_transport::AccountInfoResult {
context: ks_onchain_transport::RpcResponseContext {
slot: 11,
api_version: std::option::Option::None,
},
account: std::option::Option::Some(ks_onchain_transport::AccountInfoValue {
lamports: 100,
owner: ks_lib::MdProgramId(ks_program_ids::SYSTEM_PROGRAM_ID.to_string()),
executable: false,
rent_epoch: 0,
space: 96,
data: vec![0_u8; 96],
}),
};
let zeroed_result = crate::materialize_solana_program_metadata_account_info_result(
&request(crate::SolanaProgramMetadataExpectedAccountState::Vacant),
&zeroed,
);
assert!(matches!(
zeroed_result.as_ref().map(|value| return &value.state),
std::result::Result::Ok(
crate::SolanaProgramMetadataObservedAccountState::Uninitialized { .. }
)
));
}
#[test]
fn buffer_state_is_decoded_and_materialized_authoritatively() {
let program = solana_pubkey::Pubkey::new_from_array([4_u8; 32]);
let authority = solana_pubkey::Pubkey::new_from_array([5_u8; 32]);
let mut data = vec![0_u8; ks_lib::DC_METADATA_SPM_BUFFER_HEADER_BYTES];
data[0] = ks_lib::DcMetadataSpmAccountDiscriminator::Buffer.wire_value();
data[1..33].copy_from_slice(&program.to_bytes());
data[33..65].copy_from_slice(&authority.to_bytes());
data[65] = 1;
data[66..82].fill(7);
data.extend_from_slice(&[8_u8, 9_u8]);
let space = match u64::try_from(data.len()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let result = ks_onchain_transport::AccountInfoResult {
context: ks_onchain_transport::RpcResponseContext {
slot: 12,
api_version: std::option::Option::None,
},
account: std::option::Option::Some(ks_onchain_transport::AccountInfoValue {
lamports: 1_000,
owner: ks_lib::MdProgramId(
ks_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID.to_string(),
),
executable: false,
rent_epoch: 0,
space,
data,
}),
};
let output = crate::materialize_solana_program_metadata_account_info_result(
&request(crate::SolanaProgramMetadataExpectedAccountState::Buffer),
&result,
);
assert!(output.is_ok());
if let std::result::Result::Ok(value) = output {
assert!(matches!(
value.state,
crate::SolanaProgramMetadataObservedAccountState::Buffer(_)
));
assert_eq!(
value
.materialized_output
.as_ref()
.map(|item| return item.payload_json["projectionKind"].clone()),
std::option::Option::Some(serde_json::json!("buffer_state"))
);
}
}
#[test]
fn stateful_complete_read_bound_matches_the_transport_contract() {
assert_eq!(
crate::MAX_SOLANA_PROGRAM_METADATA_STATEFUL_ACCOUNT_BYTES,
ks_onchain_transport::MAX_COMPLETE_ACCOUNT_DATA_BYTES
);
const {
assert!(
crate::MAX_SOLANA_PROGRAM_METADATA_STATEFUL_ACCOUNT_BYTES
< ks_lib::DC_METADATA_SPM_MAX_ACCOUNT_BYTES
);
}
}
}

44
ks-pipeline/src/plan.rs Normal file
View File

@@ -0,0 +1,44 @@
// file: ks-pipeline/src/plan.rs
// version: 3
//! Pipeline planning primitives.
/// Pipeline stage identifier.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PipelineStage {
/// Ingest raw transactions.
Ingest,
/// Extract generic Solana structures.
Extract,
/// Build program observations.
Observe,
/// Decode protocol events.
Decode,
/// Materialize business events.
Materialize,
/// Aggregate materialized events.
Aggregate,
/// Validate outputs.
Validate,
}
/// Replay selection scope.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ReplayScope {
/// Optional module name.
pub module_name: std::option::Option<std::string::String>,
/// Optional module version.
pub module_version: std::option::Option<std::string::String>,
/// Optional program id.
pub program_id: std::option::Option<std::string::String>,
/// Optional surface code.
pub surface_code: std::option::Option<std::string::String>,
/// Optional 8-byte discriminator in hexadecimal.
pub discriminator_8: std::option::Option<std::string::String>,
/// Optional inclusive start slot.
pub slot_start: std::option::Option<u64>,
/// Optional inclusive end slot.
pub slot_end: std::option::Option<u64>,
/// Whether to ignore existing ledger rows.
pub force: bool,
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,393 @@
// file: ks-pipeline/src/spl_elgamal_registry_stateful.rs
// version: 6
//! 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: ks_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: &ks_onchain_transport::HttpEndpointPool,
request: &crate::ElGamalRegistryStatefulReadRequest,
) -> ks_core::Result<crate::ElGamalRegistryStatefulReadResult> {
if request.query_role.trim().is_empty() {
return std::result::Result::Err(ks_core::Error::config(
"SPL ElGamal registry stateful read query_role must not be empty",
));
}
let config = match ks_onchain_transport::GetAccountInfoConfig::new_with_data(
ks_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: &ks_onchain_transport::AccountInfoResult,
) -> ks_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(ks_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(ks_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(ks_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 != ks_program_ids::SPL_TOKEN_2022_ELGAMAL_REGISTRY_PROGRAM_ID {
return std::result::Result::Err(ks_core::Error::new(
"elgamal_registry_stateful_owner_mismatch",
format!(
"SPL ElGamal registry owner must be {}, got {}",
ks_program_ids::SPL_TOKEN_2022_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(ks_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(ks_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: ks_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 != ks_program_ids::SPL_TOKEN_2022_ELGAMAL_REGISTRY_PROGRAM_ID {
return std::result::Result::Err(format!(
"SPL ElGamal registry snapshot owner must be {}, got {owner_program_id}",
ks_program_ids::SPL_TOKEN_2022_ELGAMAL_REGISTRY_PROGRAM_ID
));
}
let state = match ks_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(
ks_program_ids::SPL_TOKEN_2022_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 ks_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(
ks_program_ids::SPL_TOKEN_2022_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(),
ks_program_ids::SPL_TOKEN_2022_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(),
ks_program_ids::SPL_TOKEN_2022_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(),
ks_program_ids::SPL_TOKEN_2022_ELGAMAL_REGISTRY_PROGRAM_ID,
55,
&data,
);
assert!(wrong.is_err());
let invalid = crate::materialize_elgamal_registry_stateful_snapshot(
registry.as_str(),
ks_program_ids::SPL_TOKEN_2022_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: ks_lib::MdPubkey(registry),
min_context_slot: std::option::Option::Some(70),
};
let result = ks_onchain_transport::AccountInfoResult {
context: ks_onchain_transport::RpcResponseContext {
slot: 71,
api_version: std::option::Option::None,
},
account: std::option::Option::Some(ks_onchain_transport::AccountInfoValue {
lamports: 1,
owner: ks_lib::MdProgramId(
ks_program_ids::SPL_TOKEN_2022_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: ks_lib::MdPubkey(registry),
min_context_slot: std::option::Option::Some(80),
};
let base = ks_onchain_transport::AccountInfoValue {
lamports: 1,
owner: ks_lib::MdProgramId(
ks_program_ids::SPL_TOKEN_2022_ELGAMAL_REGISTRY_PROGRAM_ID.to_string(),
),
executable: false,
rent_epoch: 0,
space: 64,
data: data.to_vec(),
};
let stale = ks_onchain_transport::AccountInfoResult {
context: ks_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 = ks_onchain_transport::AccountInfoResult {
context: ks_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 = ks_lib::MdProgramId(ks_program_ids::SPL_TOKEN_2022_PROGRAM_ID.to_string());
let foreign = ks_onchain_transport::AccountInfoResult {
context: ks_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 = ks_onchain_transport::AccountInfoResult {
context: ks_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 = ks_onchain_transport::AccountInfoResult {
context: ks_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,242 @@
// file: ks-pipeline/src/spl_token_2022_correlation.rs
// version: 4
//! Deterministic Token-2022 instruction-to-state correlation.
/// Correlation outcome between one committed instruction fact and one final state snapshot.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub enum Token2022CorrelationStatus {
/// The final snapshot contains the extension expected by the instruction fact.
Confirmed,
/// The final snapshot does not contain the extension expected by the instruction fact.
Contradicted,
/// The instruction output is outside the supported correlation inventory.
NotApplicable,
}
/// Deterministic correlation report for one instruction output and one final snapshot.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct Token2022StateCorrelationReport {
/// Stable caller-provided correlation identity.
pub correlation_key: std::string::String,
/// Canonical account identity checked against the snapshot.
pub account: ks_lib::MdPubkey,
/// Instruction operation or risk fact used for correlation.
pub fact_code: std::string::String,
/// Extension expected from the fact when the fact is supported.
pub expected_extension: std::option::Option<std::string::String>,
/// Final correlation outcome.
pub status: Token2022CorrelationStatus,
/// Snapshot context slot.
pub snapshot_slot: u64,
/// Whether the snapshot contains the expected extension.
pub extension_present: std::option::Option<bool>,
/// Explicit semantic limitation of this correlation.
pub fact_only: bool,
}
/// Correlates one materialized instruction fact with one authoritative Token-2022 snapshot.
pub fn correlate_token_2022_instruction_with_snapshot(
correlation_key: &str,
account: &ks_lib::MdPubkey,
instruction_output: &ks_lib::MtApiMaterializedOutput,
snapshot: &crate::Token2022StatefulSnapshotBundle,
) -> ks_core::Result<Token2022StateCorrelationReport> {
if correlation_key.trim().is_empty() {
return std::result::Result::Err(ks_core::Error::config(
"Token-2022 correlation key must not be empty",
));
}
if snapshot.account_key != account.0 {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_correlation_account_mismatch",
format!(
"Token-2022 correlation expected account {}, got snapshot {}",
account.0, snapshot.account_key
),
));
}
let fact_code = match instruction_output.payload_json.get("riskKind") {
std::option::Option::Some(value) => value.as_str().unwrap_or("").to_string(),
std::option::Option::None => instruction_output
.payload_json
.get("operation")
.and_then(serde_json::Value::as_str)
.unwrap_or("")
.to_string(),
};
if fact_code.is_empty() {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_correlation_fact_missing",
"Token-2022 correlation requires riskKind or operation in the instruction output",
));
}
let expected_extension = expected_extension(fact_code.as_str());
let extension_present = expected_extension.map(|extension| {
return snapshot.extension_names.iter().any(|candidate| return candidate == extension);
});
let status = match extension_present {
std::option::Option::Some(true) => Token2022CorrelationStatus::Confirmed,
std::option::Option::Some(false) => Token2022CorrelationStatus::Contradicted,
std::option::Option::None => Token2022CorrelationStatus::NotApplicable,
};
return std::result::Result::Ok(Token2022StateCorrelationReport {
correlation_key: correlation_key.to_string(),
account: account.clone(),
fact_code,
expected_extension: expected_extension.map(str::to_string),
status,
snapshot_slot: snapshot.slot,
extension_present,
fact_only: true,
});
}
fn expected_extension(fact_code: &str) -> std::option::Option<&'static str> {
return match fact_code {
"default_account_state_configured"
| "initialize_default_account_state"
| "update_default_account_state" => std::option::Option::Some("default_account_state"),
"required_transfer_memos_enabled"
| "required_transfer_memos_disabled"
| "enable_required_transfer_memos"
| "disable_required_transfer_memos" => std::option::Option::Some("memo_transfer"),
"cpi_guard_enabled" | "cpi_guard_disabled" | "enable_cpi_guard" | "disable_cpi_guard" => {
std::option::Option::Some("cpi_guard")
},
"non_transferable_mint_configured" | "initialize_non_transferable_mint" => {
std::option::Option::Some("non_transferable")
},
"pausable_mint_configured"
| "mint_activity_paused"
| "mint_activity_resumed"
| "initialize_pausable_config"
| "pause"
| "resume" => std::option::Option::Some("pausable"),
"permissioned_burn_configured" | "initialize_permissioned_burn" => {
std::option::Option::Some("permissioned_burn")
},
"confidential_credits_enabled"
| "confidential_credits_disabled"
| "non_confidential_credits_enabled"
| "non_confidential_credits_disabled"
| "enable_confidential_credits"
| "disable_confidential_credits"
| "enable_non_confidential_credits"
| "disable_non_confidential_credits" => {
std::option::Option::Some("confidential_transfer_account")
},
"initialize_transfer_fee_config"
| "set_transfer_fee"
| "withdraw_withheld_tokens_from_mint"
| "harvest_withheld_tokens_to_mint" => std::option::Option::Some("transfer_fee_config"),
"withdraw_withheld_tokens_from_accounts" | "transfer_checked_with_fee" => {
std::option::Option::Some("transfer_fee_amount")
},
"initialize_confidential_transfer_fee_config"
| "withdraw_confidential_withheld_tokens_from_mint"
| "harvest_confidential_withheld_tokens_to_mint"
| "enable_confidential_harvest_to_mint"
| "disable_confidential_harvest_to_mint" => {
std::option::Option::Some("confidential_transfer_fee_config")
},
"withdraw_confidential_withheld_tokens_from_accounts"
| "transfer_confidential_tokens_with_fee" => {
std::option::Option::Some("confidential_transfer_fee_amount")
},
_ => std::option::Option::None,
};
}
#[cfg(test)]
mod tests {
fn snapshot(extension_names: &[&str]) -> crate::Token2022StatefulSnapshotBundle {
return crate::Token2022StatefulSnapshotBundle {
account_key: solana_pubkey::Pubkey::new_from_array([7u8; 32]).to_string(),
slot: 42,
state_kind: "mint".to_string(),
extension_names: extension_names
.iter()
.map(|value| return (*value).to_string())
.collect(),
outputs: std::vec::Vec::new(),
};
}
fn output(field: &str, value: &str) -> ks_lib::MtApiMaterializedOutput {
let mut payload = serde_json::Map::new();
payload.insert(field.to_string(), serde_json::Value::String(value.to_string()));
return ks_lib::MtApiMaterializedOutput {
output_key: "fact:0".to_string(),
family: ks_lib::MdMaterializedEventFamily::Risk,
payload_json: serde_json::Value::Object(payload),
};
}
#[test]
fn known_fact_is_confirmed_or_contradicted_by_the_final_extension_inventory() {
let snapshot = snapshot(&["pausable"]);
let account = ks_lib::MdPubkey(snapshot.account_key.clone());
let confirmed = super::correlate_token_2022_instruction_with_snapshot(
"corr:pause",
&account,
&output("riskKind", "mint_activity_paused"),
&snapshot,
);
assert!(confirmed.is_ok());
if let std::result::Result::Ok(confirmed) = confirmed {
assert_eq!(confirmed.status, super::Token2022CorrelationStatus::Confirmed);
assert_eq!(confirmed.extension_present, std::option::Option::Some(true));
}
let contradicted = super::correlate_token_2022_instruction_with_snapshot(
"corr:fee",
&account,
&output("operation", "initialize_transfer_fee_config"),
&snapshot,
);
assert!(contradicted.is_ok());
if let std::result::Result::Ok(contradicted) = contradicted {
assert_eq!(contradicted.status, super::Token2022CorrelationStatus::Contradicted);
assert_eq!(contradicted.extension_present, std::option::Option::Some(false));
}
}
#[test]
fn unsupported_fact_is_explicitly_not_applicable_without_inventing_a_conclusion() {
let snapshot = snapshot(&[]);
let account = ks_lib::MdPubkey(snapshot.account_key.clone());
let report = super::correlate_token_2022_instruction_with_snapshot(
"corr:unknown",
&account,
&output("operation", "unknown_future_operation"),
&snapshot,
);
assert!(report.is_ok());
if let std::result::Result::Ok(report) = report {
assert_eq!(report.status, super::Token2022CorrelationStatus::NotApplicable);
assert_eq!(report.extension_present, std::option::Option::None);
assert!(report.fact_only);
}
}
#[test]
fn correlation_identity_and_account_mismatch_fail_closed() {
let snapshot = snapshot(&["memo_transfer"]);
let account = ks_lib::MdPubkey(snapshot.account_key.clone());
let empty = super::correlate_token_2022_instruction_with_snapshot(
"",
&account,
&output("riskKind", "required_transfer_memos_enabled"),
&snapshot,
);
assert!(empty.is_err());
let wrong = ks_lib::MdPubkey(solana_pubkey::Pubkey::new_from_array([8u8; 32]).to_string());
let mismatch = super::correlate_token_2022_instruction_with_snapshot(
"corr:mismatch",
&wrong,
&output("riskKind", "required_transfer_memos_enabled"),
&snapshot,
);
assert!(mismatch.is_err());
}
}

View File

@@ -0,0 +1,319 @@
// file: ks-pipeline/src/spl_token_2022_crypto_preflight.rs
// version: 5
//! Bounded cryptographic preflight for Token-2022 proof context-state accounts.
/// Maximum number of distinct proof context-state accounts accepted by one request.
pub const MAX_TOKEN_2022_PROOF_CONTEXTS: usize = 8;
/// Generic metadata bytes retained by every ZK ElGamal proof context-state account.
pub const ZK_PROOF_CONTEXT_META_BYTES: usize = 33;
/// One exact pre-verified proof context-state requirement.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Token2022ProofContextRequirement {
/// Stable semantic role used in diagnostics.
pub role: std::string::String,
/// Canonical proof context-state account.
pub account: ks_lib::MdPubkey,
/// Exact proof kind expected by the Token-2022 builder.
pub proof_type: ks_lib::ExSolanaCoreZkElGamalProofType,
/// Optional authority retained by the context-state account.
pub expected_authority: std::option::Option<ks_lib::MdPubkey>,
}
/// One bounded cryptographic preflight request.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Token2022CryptographicPreflightRequest {
/// Endpoint role used by every account read.
pub query_role: std::string::String,
/// Optional minimum RPC context slot.
pub min_context_slot: std::option::Option<u64>,
/// Ordered proof context-state requirements.
pub proof_contexts: std::vec::Vec<Token2022ProofContextRequirement>,
}
/// One validated proof context-state result.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct Token2022ProofContextReport {
/// Stable semantic role.
pub role: std::string::String,
/// Canonical proof context-state account.
pub account: ks_lib::MdPubkey,
/// Official one-byte proof discriminator.
pub proof_discriminator: u8,
/// Exact context-state size for the proof type.
pub context_state_bytes: usize,
/// Context slot returned by the endpoint.
pub context_slot: u64,
/// Ordered successful checks.
pub checks: std::vec::Vec<std::string::String>,
}
/// Complete cryptographic preflight report.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct Token2022CryptographicPreflightReport {
/// Commitment used for all reads.
pub commitment: std::string::String,
/// Highest context slot observed across all reads.
pub context_slot: u64,
/// Ordered validated proof contexts.
pub proof_contexts: std::vec::Vec<Token2022ProofContextReport>,
}
/// Reads and validates all pre-verified proof context-state accounts required by one operation.
pub async fn inspect_token_2022_cryptographic_preflight(
pool: &ks_onchain_transport::HttpEndpointPool,
request: &crate::Token2022CryptographicPreflightRequest,
) -> ks_core::Result<crate::Token2022CryptographicPreflightReport> {
let requirements = match validate_proof_context_requirements(request) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut context_slot = request.min_context_slot.unwrap_or(0);
let mut reports = std::vec::Vec::with_capacity(requirements.len());
for requirement in requirements {
let expected_size = requirement.proof_type.context_state_size();
let config = match ks_onchain_transport::GetAccountInfoConfig::new_with_data(
ks_onchain_transport::RpcCommitmentLevel::Confirmed,
request.min_context_slot,
expected_size,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let result = match pool
.get_account_info_for_role(request.query_role.as_str(), &requirement.account, &config)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let report = match validate_proof_context_account(&requirement, &result) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
context_slot = context_slot.max(report.context_slot);
reports.push(report);
}
return std::result::Result::Ok(crate::Token2022CryptographicPreflightReport {
commitment: "confirmed".to_string(),
context_slot,
proof_contexts: reports,
});
}
fn validate_proof_context_requirements(
request: &crate::Token2022CryptographicPreflightRequest,
) -> ks_core::Result<std::vec::Vec<crate::Token2022ProofContextRequirement>> {
if request.query_role.trim().is_empty() {
return std::result::Result::Err(ks_core::Error::config(
"Token-2022 cryptographic preflight query_role must not be empty",
));
}
if request.proof_contexts.len() > crate::MAX_TOKEN_2022_PROOF_CONTEXTS {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_proof_context_limit_exceeded",
format!(
"Token-2022 cryptographic preflight accepts at most {} proof contexts",
crate::MAX_TOKEN_2022_PROOF_CONTEXTS
),
));
}
let mut indexes = std::collections::BTreeMap::<std::string::String, usize>::new();
let mut unique = std::vec::Vec::<crate::Token2022ProofContextRequirement>::new();
for requirement in &request.proof_contexts {
if requirement.role.trim().is_empty() {
return std::result::Result::Err(ks_core::Error::config(
"Token-2022 proof context role must not be empty",
));
}
if let std::option::Option::Some(index) = indexes.get(requirement.account.0.as_str()) {
if &unique[*index] != requirement {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_proof_context_conflicting_duplicate",
format!(
"Token-2022 proof context {} has conflicting requirements",
requirement.account.0
),
));
}
continue;
}
indexes.insert(requirement.account.0.clone(), unique.len());
unique.push(requirement.clone());
}
return std::result::Result::Ok(unique);
}
fn validate_proof_context_account(
requirement: &crate::Token2022ProofContextRequirement,
result: &ks_onchain_transport::AccountInfoResult,
) -> ks_core::Result<crate::Token2022ProofContextReport> {
let account = match result.account.as_ref() {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_proof_context_missing",
format!(
"Token-2022 proof context account {} does not exist",
requirement.account.0
),
));
},
};
if account.owner.0 != ks_program_ids::ZK_ELGAMAL_PROOF_PROGRAM_ID {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_proof_context_owner_mismatch",
format!(
"Token-2022 proof context owner must be {}, got {}",
ks_program_ids::ZK_ELGAMAL_PROOF_PROGRAM_ID,
account.owner.0
),
));
}
if account.executable {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_proof_context_executable",
"Token-2022 proof context account must not be executable",
));
}
let expected_size = requirement.proof_type.context_state_size();
if account.space != expected_size as u64 || account.data.len() != expected_size {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_proof_context_size_mismatch",
format!(
"Token-2022 proof context {} must contain exactly {expected_size} bytes, got space {} and data {}",
requirement.account.0,
account.space,
account.data.len()
),
));
}
if account.data.len() < crate::ZK_PROOF_CONTEXT_META_BYTES {
return std::result::Result::Err(ks_core::Error::invalid_state(
"Token-2022 proof context is shorter than its generic metadata",
));
}
let discriminator = account.data[32];
if discriminator != requirement.proof_type.discriminator() {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_proof_context_type_mismatch",
format!(
"Token-2022 proof context {} discriminator {} does not match expected {}",
requirement.account.0,
discriminator,
requirement.proof_type.discriminator()
),
));
}
let mut checks = std::vec![
"zk_program_owner".to_string(),
"not_executable".to_string(),
"exact_context_state_size".to_string(),
"proof_type_discriminator".to_string(),
];
if let std::option::Option::Some(expected_authority) = requirement.expected_authority.as_ref() {
let retained_authority = bs58::encode(&account.data[..32]).into_string();
if retained_authority != expected_authority.0 {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_proof_context_authority_mismatch",
format!(
"Token-2022 proof context {} retains authority {}, expected {}",
requirement.account.0, retained_authority, expected_authority.0
),
));
}
checks.push("retained_authority".to_string());
}
return std::result::Result::Ok(crate::Token2022ProofContextReport {
role: requirement.role.clone(),
account: requirement.account.clone(),
proof_discriminator: discriminator,
context_state_bytes: expected_size,
context_slot: result.context.slot,
checks,
});
}
#[cfg(test)]
mod tests {
fn pubkey(byte: u8) -> ks_lib::MdPubkey {
return ks_lib::MdPubkey(bs58::encode([byte; 32]).into_string());
}
fn requirement(
account: ks_lib::MdPubkey,
proof_type: ks_lib::ExSolanaCoreZkElGamalProofType,
) -> crate::Token2022ProofContextRequirement {
return crate::Token2022ProofContextRequirement {
role: "equality_proof".to_string(),
account,
proof_type,
expected_authority: std::option::Option::Some(pubkey(9)),
};
}
fn account_result(
proof_type: ks_lib::ExSolanaCoreZkElGamalProofType,
) -> ks_onchain_transport::AccountInfoResult {
let mut data = std::vec![0u8; proof_type.context_state_size()];
data[..32].copy_from_slice(&[9u8; 32]);
data[32] = proof_type.discriminator();
return ks_onchain_transport::AccountInfoResult {
context: ks_onchain_transport::RpcResponseContext {
slot: 77,
api_version: std::option::Option::None,
},
account: std::option::Option::Some(ks_onchain_transport::AccountInfoValue {
lamports: 1,
owner: ks_lib::MdProgramId(ks_program_ids::ZK_ELGAMAL_PROOF_PROGRAM_ID.to_string()),
executable: false,
rent_epoch: 0,
space: data.len() as u64,
data,
}),
};
}
#[test]
fn exact_duplicates_are_deduplicated_and_conflicts_fail_closed() {
let item = requirement(
pubkey(1),
ks_lib::ExSolanaCoreZkElGamalProofType::CiphertextCiphertextEquality,
);
let request = crate::Token2022CryptographicPreflightRequest {
query_role: "query".to_string(),
min_context_slot: std::option::Option::Some(7),
proof_contexts: std::vec![item.clone(), item.clone()],
};
let unique = super::validate_proof_context_requirements(&request);
assert_eq!(unique.as_ref().map(std::vec::Vec::len), std::result::Result::Ok(1));
let conflict = crate::Token2022CryptographicPreflightRequest {
query_role: "query".to_string(),
min_context_slot: std::option::Option::None,
proof_contexts: std::vec![
item,
requirement(
pubkey(1),
ks_lib::ExSolanaCoreZkElGamalProofType::BatchedRangeProofU128,
),
],
};
assert!(super::validate_proof_context_requirements(&conflict).is_err());
}
#[test]
fn proof_context_validation_checks_owner_size_type_and_authority() {
let proof_type = ks_lib::ExSolanaCoreZkElGamalProofType::CiphertextCiphertextEquality;
let context_requirement = requirement(pubkey(1), proof_type);
let result = account_result(proof_type);
let report = super::validate_proof_context_account(&context_requirement, &result);
assert_eq!(
report.as_ref().map(|value| return value.checks.len()),
std::result::Result::Ok(5)
);
let wrong_type =
requirement(pubkey(1), ks_lib::ExSolanaCoreZkElGamalProofType::BatchedRangeProofU128);
assert!(super::validate_proof_context_account(&wrong_type, &result,).is_err());
}
}

View File

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

View File

@@ -0,0 +1,376 @@
// file: ks-pipeline/src/spl_token_2022_metadata.rs
// version: 4
//! Token-2022 Token Metadata return-data and authority postcondition contracts.
use base64::Engine; // rust-rules: trait-import
/// Maximum Token Metadata bytes accepted from Solana return data.
pub const MAX_TOKEN_2022_METADATA_EMIT_BYTES: usize = 1_024;
/// Bounded evidence extracted from one successful Token Metadata `Emit` simulation.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct Token2022MetadataEmitEvidence {
/// Program that produced the return data.
pub program_id: std::string::String,
/// Optional requested range start.
pub start: std::option::Option<u64>,
/// Optional requested range end.
pub end: std::option::Option<u64>,
/// Decoded return-data bytes.
pub data: std::vec::Vec<u8>,
/// Exact complete Token Metadata projection, only for an un-ranged emit.
pub decoded_metadata: std::option::Option<serde_json::Value>,
}
/// Validates one Token Metadata `Emit` range against Solana return-data bounds.
pub fn validate_token_2022_metadata_emit_range(
start: std::option::Option<u64>,
end: std::option::Option<u64>,
) -> ks_core::Result<()> {
if let (std::option::Option::Some(start), std::option::Option::Some(end)) = (start, end) {
if start > end {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_metadata_emit_range_invalid",
format!("Token Metadata Emit start {start} exceeds end {end}"),
));
}
if end - start > crate::MAX_TOKEN_2022_METADATA_EMIT_BYTES as u64 {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_metadata_emit_range_too_large",
format!(
"Token Metadata Emit range exceeds {} bytes",
crate::MAX_TOKEN_2022_METADATA_EMIT_BYTES
),
));
}
}
if let (std::option::Option::None, std::option::Option::Some(end)) = (start, end) {
if end > crate::MAX_TOKEN_2022_METADATA_EMIT_BYTES as u64 {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_metadata_emit_range_too_large",
format!(
"Token Metadata Emit range from zero exceeds {} bytes",
crate::MAX_TOKEN_2022_METADATA_EMIT_BYTES
),
));
}
}
if let (std::option::Option::Some(_), std::option::Option::None) = (start, end) {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_metadata_emit_range_end_required",
"Token Metadata Emit with a start offset requires an explicit end to remain bounded",
));
}
return std::result::Result::Ok(());
}
/// Extracts and validates Token Metadata return data from one exact simulation.
pub fn inspect_token_2022_metadata_emit_simulation(
simulation: &ks_lib::ExApiExecutionSimulationResult,
start: std::option::Option<u64>,
end: std::option::Option<u64>,
) -> ks_core::Result<crate::Token2022MetadataEmitEvidence> {
match crate::validate_token_2022_metadata_emit_range(start, end) {
std::result::Result::Ok(()) => {},
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
if !simulation.simulated || !simulation.success {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_metadata_emit_simulation_required",
"Token Metadata Emit evidence requires a successful simulation",
));
}
let return_data = match simulation.return_data.as_ref() {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_metadata_emit_return_data_missing",
"Token Metadata Emit simulation did not return program data",
));
},
};
let program_id = match return_data.get("programId").and_then(serde_json::Value::as_str) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_metadata_emit_program_id_missing",
"Token Metadata Emit return data is missing programId",
));
},
};
if program_id != ks_program_ids::SPL_TOKEN_2022_PROGRAM_ID {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_metadata_emit_program_id_mismatch",
format!("Token Metadata Emit return data came from {program_id}"),
));
}
let data = match return_data.get("data").and_then(serde_json::Value::as_array) {
std::option::Option::Some(value) if value.len() == 2 => value,
_ => {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_metadata_emit_data_invalid",
"Token Metadata Emit return data must be [payload, encoding]",
));
},
};
let payload = match data.first().and_then(|value| return value.as_str()) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_metadata_emit_payload_invalid",
"Token Metadata Emit return payload must be a base64 string",
));
},
};
let encoding = match data.get(1).and_then(|value| return value.as_str()) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_metadata_emit_encoding_invalid",
"Token Metadata Emit return data encoding is missing",
));
},
};
if encoding != "base64" {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_metadata_emit_encoding_invalid",
"Token Metadata Emit return data encoding must be base64",
));
}
let decoded = match base64::engine::general_purpose::STANDARD.decode(payload) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_metadata_emit_base64_invalid",
format!("Token Metadata Emit return payload is invalid base64: {error}"),
));
},
};
if decoded.len() > crate::MAX_TOKEN_2022_METADATA_EMIT_BYTES {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_metadata_emit_data_too_large",
format!(
"Token Metadata Emit returned {} bytes, maximum is {}",
decoded.len(),
crate::MAX_TOKEN_2022_METADATA_EMIT_BYTES
),
));
}
let decoded_metadata = if start.is_none() && end.is_none() {
match ks_lib::decoder_spl_token_2022_parse_token_metadata_value(decoded.as_slice()) {
std::result::Result::Ok(value) => std::option::Option::Some(value),
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_metadata_emit_payload_decode_failed",
error,
));
},
}
} else {
std::option::Option::None
};
return std::result::Result::Ok(crate::Token2022MetadataEmitEvidence {
program_id: program_id.to_string(),
start,
end,
data: decoded,
decoded_metadata,
});
}
/// Checks the final embedded metadata authority against one authoritative snapshot.
pub fn inspect_token_2022_metadata_authority_postcondition(
account: &ks_lib::MdPubkey,
expected_authority: std::option::Option<&ks_lib::MdPubkey>,
snapshot: &crate::Token2022StatefulSnapshotBundle,
) -> crate::Token2022ExecutionPostcondition {
if snapshot.account_key != account.0 {
return crate::Token2022ExecutionPostcondition {
role: "metadata".to_string(),
account: account.clone(),
status: crate::Token2022ExecutionPostconditionStatus::Contradicted,
diagnostic: "Token-2022 metadata snapshot belongs to a different account".to_string(),
};
}
let output = snapshot.outputs.iter().find(|output| {
return output.family == ks_lib::MdMaterializedEventFamily::Metadata
&& output.payload_json.get("projectionKind").and_then(serde_json::Value::as_str)
== std::option::Option::Some("token_metadata");
});
let observed = output.and_then(|value| {
return value.payload_json.get("valueFields").and_then(|fields| {
return fields.get("updateAuthority");
});
});
let confirmed = match (expected_authority, observed) {
(std::option::Option::Some(expected), std::option::Option::Some(value)) => {
value.as_str() == std::option::Option::Some(expected.0.as_str())
},
(std::option::Option::None, std::option::Option::Some(value)) => value.is_null(),
_ => false,
};
return crate::Token2022ExecutionPostcondition {
role: "metadata".to_string(),
account: account.clone(),
status: if confirmed {
crate::Token2022ExecutionPostconditionStatus::Confirmed
} else {
crate::Token2022ExecutionPostconditionStatus::Contradicted
},
diagnostic: if confirmed {
"authoritative Token-2022 metadata snapshot matches the expected update authority"
.to_string()
} else {
"authoritative Token-2022 metadata snapshot contradicts or omits the expected update authority"
.to_string()
},
};
}
#[cfg(test)]
mod tests {
use base64::Engine; // rust-rules: trait-import
fn simulation(data: &[u8]) -> ks_lib::ExApiExecutionSimulationResult {
return ks_lib::ExApiExecutionSimulationResult {
simulated: true,
success: true,
cluster: ks_lib::ExApiExecutionCluster::Devnet,
blockhash_kind: ks_lib::ExApiExecutionBlockhashKind::Latest,
blockhash_age_slots: std::option::Option::Some(0),
replacement_blockhash: std::option::Option::None,
replacement_last_valid_block_height: std::option::Option::None,
nonce_account: std::option::Option::None,
nonce_authority: std::option::Option::None,
units_consumed: std::option::Option::Some(1),
estimated_fee_lamports: std::option::Option::Some(5_000),
logs: std::vec::Vec::new(),
return_data: std::option::Option::Some(serde_json::json!({
"programId": ks_program_ids::SPL_TOKEN_2022_PROGRAM_ID,
"data": [base64::engine::general_purpose::STANDARD.encode(data), "base64"]
})),
error: std::option::Option::None,
};
}
fn metadata_bytes(authority: std::option::Option<[u8; 32]>) -> std::vec::Vec<u8> {
let mut value = std::vec::Vec::new();
value.extend_from_slice(authority.unwrap_or([0_u8; 32]).as_slice());
value.extend_from_slice([7_u8; 32].as_slice());
for field in ["Token", "TKN", "https://example.invalid/token.json"] {
value.extend_from_slice((field.len() as u32).to_le_bytes().as_slice());
value.extend_from_slice(field.as_bytes());
}
value.extend_from_slice(0_u32.to_le_bytes().as_slice());
return value;
}
#[test]
fn complete_emit_decodes_exact_metadata_and_ranged_emit_keeps_only_bytes() {
let complete = crate::inspect_token_2022_metadata_emit_simulation(
&simulation(metadata_bytes(std::option::Option::Some([9_u8; 32])).as_slice()),
std::option::Option::None,
std::option::Option::None,
)
.unwrap_or_else(|error| panic!("complete Emit evidence failed: {error}"));
assert_eq!(
complete
.decoded_metadata
.as_ref()
.and_then(|value| return value.get("name"))
.and_then(serde_json::Value::as_str),
std::option::Option::Some("Token")
);
let ranged = crate::inspect_token_2022_metadata_emit_simulation(
&simulation(&[1_u8, 2_u8, 3_u8]),
std::option::Option::Some(0),
std::option::Option::Some(3),
)
.unwrap_or_else(|error| panic!("ranged Emit evidence failed: {error}"));
assert!(ranged.decoded_metadata.is_none());
assert_eq!(ranged.data, vec![1_u8, 2_u8, 3_u8]);
}
#[test]
fn emit_range_and_provider_contract_fail_closed() {
assert!(
crate::validate_token_2022_metadata_emit_range(
std::option::Option::Some(2),
std::option::Option::Some(1),
)
.is_err()
);
assert!(
crate::validate_token_2022_metadata_emit_range(
std::option::Option::Some(0),
std::option::Option::Some(1_025),
)
.is_err()
);
assert!(
crate::validate_token_2022_metadata_emit_range(
std::option::Option::Some(1),
std::option::Option::None,
)
.is_err()
);
let mut invalid = simulation(&[1_u8]);
invalid.return_data = std::option::Option::Some(serde_json::json!({
"programId": ks_program_ids::SYSTEM_PROGRAM_ID,
"data": ["AQ==", "base64"]
}));
assert!(
crate::inspect_token_2022_metadata_emit_simulation(
&invalid,
std::option::Option::Some(0),
std::option::Option::Some(1)
)
.is_err()
);
}
#[test]
fn authority_postcondition_accepts_exact_value_and_null() {
let account = ks_lib::MdPubkey(bs58::encode([3_u8; 32]).into_string());
let authority = ks_lib::MdPubkey(bs58::encode([4_u8; 32]).into_string());
let mut snapshot = crate::Token2022StatefulSnapshotBundle {
account_key: account.0.clone(),
slot: 9,
state_kind: "mint".to_string(),
extension_names: vec!["token_metadata".to_string()],
outputs: vec![ks_lib::MtApiMaterializedOutput {
output_key: "metadata".to_string(),
family: ks_lib::MdMaterializedEventFamily::Metadata,
payload_json: serde_json::json!({
"projectionKind": "token_metadata",
"valueFields": {"updateAuthority": authority.0}
}),
}],
};
let confirmed = crate::inspect_token_2022_metadata_authority_postcondition(
&account,
std::option::Option::Some(&authority),
&snapshot,
);
assert_eq!(confirmed.status, crate::Token2022ExecutionPostconditionStatus::Confirmed);
snapshot.outputs[0].payload_json["valueFields"]["updateAuthority"] =
serde_json::Value::Null;
let cleared = crate::inspect_token_2022_metadata_authority_postcondition(
&account,
std::option::Option::None,
&snapshot,
);
assert_eq!(cleared.status, crate::Token2022ExecutionPostconditionStatus::Confirmed);
snapshot.account_key = bs58::encode([8_u8; 32]).into_string();
let wrong_account = crate::inspect_token_2022_metadata_authority_postcondition(
&account,
std::option::Option::None,
&snapshot,
);
assert_eq!(
wrong_account.status,
crate::Token2022ExecutionPostconditionStatus::Contradicted
);
}
}

View File

@@ -0,0 +1,431 @@
// file: ks-pipeline/src/spl_token_2022_preflight.rs
// version: 5
//! Bounded Token-2022 stateful preflight orchestration.
/// Maximum distinct Token-2022 accounts accepted by one preflight inspection.
pub const MAX_TOKEN_2022_PREFLIGHT_ACCOUNTS: usize = 16;
/// Maximum aggregate account-data budget accepted by one preflight inspection.
pub const MAX_TOKEN_2022_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: ks_lib::MdPubkey,
/// Expected Token-2022 state category.
pub kind: ks_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<ks_lib::MdPubkey>,
/// Optional expected owner for a Token Account.
pub expected_owner: std::option::Option<ks_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: ks_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_token_2022_preflight(
pool: &ks_onchain_transport::HttpEndpointPool,
request: &crate::Token2022PreflightRequest,
) -> ks_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_token_2022_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(ks_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,
) -> ks_core::Result<std::vec::Vec<crate::Token2022PreflightRequirement>> {
if request.query_role.trim().is_empty() {
return std::result::Result::Err(ks_core::Error::config(
"Token-2022 preflight query_role must not be empty",
));
}
if request.max_accounts == 0 || request.max_accounts > crate::MAX_TOKEN_2022_PREFLIGHT_ACCOUNTS
{
return std::result::Result::Err(ks_core::Error::config(format!(
"Token-2022 preflight max_accounts must be between 1 and {}",
crate::MAX_TOKEN_2022_PREFLIGHT_ACCOUNTS
)));
}
if request.max_total_data_bytes == 0
|| request.max_total_data_bytes > crate::MAX_TOKEN_2022_PREFLIGHT_TOTAL_BYTES
{
return std::result::Result::Err(ks_core::Error::config(format!(
"Token-2022 preflight max_total_data_bytes must be between 1 and {}",
crate::MAX_TOKEN_2022_PREFLIGHT_TOTAL_BYTES
)));
}
if request.requirements.is_empty() {
return std::result::Result::Err(ks_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(ks_core::Error::config(
"Token-2022 preflight requirement role must not be empty",
));
}
if requirement.max_data_bytes == 0
|| requirement.max_data_bytes > crate::MAX_TOKEN_2022_STATEFUL_ACCOUNT_BYTES
{
return std::result::Result::Err(ks_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(ks_core::Error::new(
"token_2022_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(ks_core::Error::new(
"token_2022_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(ks_core::Error::new(
"token_2022_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,
) -> ks_core::Result<crate::Token2022PreflightAccountReport> {
let token_output = result
.snapshot
.outputs
.iter()
.find(|output| return output.payload_json["domain"] == "spl_token_2022_account_state");
let token_output = match token_output {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ks_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(ks_core::Error::new(
"token_2022_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(ks_core::Error::new(
"token_2022_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(ks_core::Error::new(
"token_2022_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(ks_core::Error::new(
"token_2022_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) -> ks_lib::MdPubkey {
return ks_lib::MdPubkey(bs58::encode([byte; 32]).into_string());
}
fn requirement(account: ks_lib::MdPubkey) -> crate::Token2022PreflightRequirement {
return crate::Token2022PreflightRequirement {
role: "source".to_string(),
account,
kind: ks_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: ks_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![ks_lib::MtApiMaterializedOutput {
output_key: "state".to_string(),
family: ks_lib::MdMaterializedEventFamily::TokenAccount,
payload_json: serde_json::json!({
"domain":"spl_token_2022_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,373 @@
// file: ks-pipeline/src/spl_token_2022_proof_orchestration.rs
// version: 6
//! Deterministic orchestration contract for mixed Token-2022 proof locations.
/// Maximum proof references accepted by one confidential Token-2022 operation.
pub const MAX_TOKEN_2022_OPERATION_PROOFS: usize = 5;
/// One bounded proof-orchestration request prepared before transaction assembly.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Token2022ProofOrchestrationRequest {
/// Stable executor operation code.
pub operation_code: std::string::String,
/// Exact ordered proof references required by the operation.
pub proofs: std::vec::Vec<ks_lib::ExSplTokenConfidentialProofReference>,
/// Optional authority expected on every context-state account.
pub expected_context_authority: std::option::Option<ks_lib::MdPubkey>,
/// Maximum compute-unit limit accepted for the future transaction.
pub compute_unit_limit: u32,
/// Maximum total fee accepted for the future transaction.
pub max_fee_lamports: u64,
/// Whether a future submission was explicitly requested.
pub submit: bool,
/// Whether the operator explicitly confirmed the future submission.
pub operator_confirmed: bool,
}
/// One deterministic mixed-proof orchestration result.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Token2022ProofOrchestrationReport {
/// Stable executor operation code.
pub operation_code: std::string::String,
/// Whether the instructions sysvar must be present.
pub instructions_sysvar_required: bool,
/// Ordered non-zero inline offsets.
pub inline_offsets: std::vec::Vec<i8>,
/// Ordered validated context-state requirements.
pub context_requirements: std::vec::Vec<crate::Token2022ProofContextRequirement>,
/// Simulation is always mandatory.
pub simulation_required: bool,
/// Whether the future send path is authorized by request-local policy.
pub send_authorized: bool,
/// Ordered successful checks.
pub checks: std::vec::Vec<std::string::String>,
}
/// Validates mixed inline/context-state proof orchestration before transaction assembly.
pub fn orchestrate_token_2022_proofs(
request: &crate::Token2022ProofOrchestrationRequest,
cryptographic_preflight: &crate::Token2022CryptographicPreflightReport,
) -> ks_core::Result<crate::Token2022ProofOrchestrationReport> {
if request.operation_code.trim().is_empty() {
return std::result::Result::Err(ks_core::Error::config(
"Token-2022 proof orchestration operation_code must not be empty",
));
}
if request.proofs.len() > crate::MAX_TOKEN_2022_OPERATION_PROOFS {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_operation_proof_limit_exceeded",
format!(
"Token-2022 proof orchestration accepts at most {} proofs",
crate::MAX_TOKEN_2022_OPERATION_PROOFS
),
));
}
if request.compute_unit_limit == 0 || request.max_fee_lamports == 0 {
return std::result::Result::Err(ks_core::Error::config(
"Token-2022 proof orchestration requires non-zero compute and fee ceilings",
));
}
if request.submit && !request.operator_confirmed {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_submission_confirmation_required",
"Token-2022 submission requires explicit operator confirmation",
));
}
let mut kinds = std::collections::BTreeSet::<std::string::String>::new();
let mut offsets = std::collections::BTreeSet::<i8>::new();
let mut context_accounts = std::collections::BTreeSet::<std::string::String>::new();
let mut inline_offsets = std::vec::Vec::<i8>::new();
let mut context_requirements = std::vec::Vec::<crate::Token2022ProofContextRequirement>::new();
for proof in &request.proofs {
let kind_code = proof_kind_code(proof.kind).to_string();
if !kinds.insert(kind_code.clone()) {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_duplicate_proof_kind",
format!(
"Token-2022 operation {} contains duplicate proof kind {kind_code}",
request.operation_code
),
));
}
if let std::result::Result::Err(error) = proof.location.validate() {
return std::result::Result::Err(ks_core::Error::invalid_state(error));
}
match &proof.location {
ks_lib::ExSplTokenConfidentialProofLocation::InstructionOffset { offset } => {
if !offsets.insert(*offset) {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_duplicate_inline_proof_offset",
format!(
"Token-2022 operation {} reuses inline proof offset {offset}",
request.operation_code
),
));
}
inline_offsets.push(*offset);
},
ks_lib::ExSplTokenConfidentialProofLocation::ContextStateAccount { account } => {
if !context_accounts.insert(account.0.clone()) {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_duplicate_context_state_account",
format!(
"Token-2022 operation {} reuses context-state account {}",
request.operation_code, account.0
),
));
}
context_requirements.push(crate::Token2022ProofContextRequirement {
role: kind_code,
account: account.clone(),
proof_type: proof_kind_to_zk_type(proof.kind),
expected_authority: request.expected_context_authority.clone(),
});
},
}
}
if let std::result::Result::Err(error) =
validate_context_reports(context_requirements.as_slice(), cryptographic_preflight)
{
return std::result::Result::Err(error);
}
return std::result::Result::Ok(crate::Token2022ProofOrchestrationReport {
operation_code: request.operation_code.clone(),
instructions_sysvar_required: !inline_offsets.is_empty(),
inline_offsets,
context_requirements,
simulation_required: true,
send_authorized: request.submit && request.operator_confirmed,
checks: vec![
"ordered_unique_proof_kinds".to_string(),
"non_zero_unique_inline_offsets".to_string(),
"unique_context_state_accounts".to_string(),
"context_preflight_matches_operation".to_string(),
"simulation_required".to_string(),
"bounded_compute_and_fee".to_string(),
],
});
}
fn validate_context_reports(
requirements: &[crate::Token2022ProofContextRequirement],
report: &crate::Token2022CryptographicPreflightReport,
) -> ks_core::Result<()> {
if requirements.len() != report.proof_contexts.len() {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_context_preflight_count_mismatch",
format!(
"Token-2022 operation requires {} context states, preflight contains {}",
requirements.len(),
report.proof_contexts.len()
),
));
}
for (requirement, observed) in requirements.iter().zip(report.proof_contexts.iter()) {
if requirement.account != observed.account
|| requirement.proof_type.discriminator() != observed.proof_discriminator
{
return std::result::Result::Err(ks_core::Error::new(
"token_2022_context_preflight_mismatch",
format!(
"Token-2022 proof context {} does not match the ordered operation requirement",
observed.account.0
),
));
}
}
return std::result::Result::Ok(());
}
fn proof_kind_code(kind: ks_lib::ExSplTokenConfidentialProofKind) -> &'static str {
return match kind {
ks_lib::ExSplTokenConfidentialProofKind::PubkeyValidity => "pubkey_validity",
ks_lib::ExSplTokenConfidentialProofKind::ZeroCiphertext => "zero_ciphertext",
ks_lib::ExSplTokenConfidentialProofKind::CiphertextCommitmentEquality => {
"ciphertext_commitment_equality"
},
ks_lib::ExSplTokenConfidentialProofKind::CiphertextCiphertextEquality => {
"ciphertext_ciphertext_equality"
},
ks_lib::ExSplTokenConfidentialProofKind::BatchedGroupedCiphertext3HandlesValidity => {
"batched_grouped_ciphertext_3_handles_validity"
},
ks_lib::ExSplTokenConfidentialProofKind::BatchedGroupedCiphertext2HandlesValidity => {
"batched_grouped_ciphertext_2_handles_validity"
},
ks_lib::ExSplTokenConfidentialProofKind::PercentageWithFee => "percentage_with_fee",
ks_lib::ExSplTokenConfidentialProofKind::BatchedRangeProofU64 => "batched_range_proof_u64",
ks_lib::ExSplTokenConfidentialProofKind::BatchedRangeProofU128 => {
"batched_range_proof_u128"
},
ks_lib::ExSplTokenConfidentialProofKind::BatchedRangeProofU256 => {
"batched_range_proof_u256"
},
};
}
fn proof_kind_to_zk_type(
kind: ks_lib::ExSplTokenConfidentialProofKind,
) -> ks_lib::ExSolanaCoreZkElGamalProofType {
return match kind {
ks_lib::ExSplTokenConfidentialProofKind::PubkeyValidity => {
ks_lib::ExSolanaCoreZkElGamalProofType::PubkeyValidity
},
ks_lib::ExSplTokenConfidentialProofKind::ZeroCiphertext => {
ks_lib::ExSolanaCoreZkElGamalProofType::ZeroCiphertext
},
ks_lib::ExSplTokenConfidentialProofKind::CiphertextCommitmentEquality => {
ks_lib::ExSolanaCoreZkElGamalProofType::CiphertextCommitmentEquality
},
ks_lib::ExSplTokenConfidentialProofKind::CiphertextCiphertextEquality => {
ks_lib::ExSolanaCoreZkElGamalProofType::CiphertextCiphertextEquality
},
ks_lib::ExSplTokenConfidentialProofKind::BatchedGroupedCiphertext3HandlesValidity => {
ks_lib::ExSolanaCoreZkElGamalProofType::BatchedGroupedCiphertext3HandlesValidity
},
ks_lib::ExSplTokenConfidentialProofKind::BatchedGroupedCiphertext2HandlesValidity => {
ks_lib::ExSolanaCoreZkElGamalProofType::BatchedGroupedCiphertext2HandlesValidity
},
ks_lib::ExSplTokenConfidentialProofKind::PercentageWithFee => {
ks_lib::ExSolanaCoreZkElGamalProofType::PercentageWithCap
},
ks_lib::ExSplTokenConfidentialProofKind::BatchedRangeProofU64 => {
ks_lib::ExSolanaCoreZkElGamalProofType::BatchedRangeProofU64
},
ks_lib::ExSplTokenConfidentialProofKind::BatchedRangeProofU128 => {
ks_lib::ExSolanaCoreZkElGamalProofType::BatchedRangeProofU128
},
ks_lib::ExSplTokenConfidentialProofKind::BatchedRangeProofU256 => {
ks_lib::ExSolanaCoreZkElGamalProofType::BatchedRangeProofU256
},
};
}
#[cfg(test)]
mod tests {
fn pubkey(byte: u8) -> ks_lib::MdPubkey {
return ks_lib::MdPubkey(bs58::encode([byte; 32]).into_string());
}
fn context_report(
account: ks_lib::MdPubkey,
proof_type: ks_lib::ExSolanaCoreZkElGamalProofType,
) -> crate::Token2022ProofContextReport {
return crate::Token2022ProofContextReport {
role: "proof".to_string(),
account,
proof_discriminator: proof_type.discriminator(),
context_state_bytes: proof_type.context_state_size(),
context_slot: 100,
checks: vec!["validated".to_string()],
};
}
#[test]
fn mixed_proofs_require_sysvar_and_preserve_ordered_contexts() {
let context = pubkey(1);
let request = crate::Token2022ProofOrchestrationRequest {
operation_code: "spl.token_2022.confidential_transfer".to_string(),
proofs: vec![
ks_lib::ExSplTokenConfidentialProofReference {
kind: ks_lib::ExSplTokenConfidentialProofKind::CiphertextCommitmentEquality,
location: ks_lib::ExSplTokenConfidentialProofLocation::InstructionOffset {
offset: 1,
},
},
ks_lib::ExSplTokenConfidentialProofReference {
kind: ks_lib::ExSplTokenConfidentialProofKind::BatchedRangeProofU128,
location: ks_lib::ExSplTokenConfidentialProofLocation::ContextStateAccount {
account: context.clone(),
},
},
],
expected_context_authority: std::option::Option::None,
compute_unit_limit: 400_000,
max_fee_lamports: 50_000,
submit: false,
operator_confirmed: false,
};
let preflight = crate::Token2022CryptographicPreflightReport {
commitment: "confirmed".to_string(),
context_slot: 100,
proof_contexts: vec![context_report(
context,
ks_lib::ExSolanaCoreZkElGamalProofType::BatchedRangeProofU128,
)],
};
let report = crate::orchestrate_token_2022_proofs(&request, &preflight)
.expect("mixed proof orchestration must succeed");
assert!(report.instructions_sysvar_required);
assert_eq!(report.inline_offsets, vec![1]);
assert_eq!(report.context_requirements.len(), 1);
assert!(report.simulation_required);
assert!(!report.send_authorized);
}
#[test]
fn context_only_proofs_do_not_require_instructions_sysvar() {
let context = pubkey(2);
let request = crate::Token2022ProofOrchestrationRequest {
operation_code: "spl.token_2022.empty_confidential_account".to_string(),
proofs: vec![ks_lib::ExSplTokenConfidentialProofReference {
kind: ks_lib::ExSplTokenConfidentialProofKind::ZeroCiphertext,
location: ks_lib::ExSplTokenConfidentialProofLocation::ContextStateAccount {
account: context.clone(),
},
}],
expected_context_authority: std::option::Option::None,
compute_unit_limit: 200_000,
max_fee_lamports: 20_000,
submit: true,
operator_confirmed: true,
};
let preflight = crate::Token2022CryptographicPreflightReport {
commitment: "confirmed".to_string(),
context_slot: 100,
proof_contexts: vec![context_report(
context,
ks_lib::ExSolanaCoreZkElGamalProofType::ZeroCiphertext,
)],
};
let report = crate::orchestrate_token_2022_proofs(&request, &preflight)
.expect("context-only orchestration must succeed");
assert!(!report.instructions_sysvar_required);
assert!(report.send_authorized);
}
#[test]
fn duplicate_offsets_contexts_and_unconfirmed_submission_fail_closed() {
let duplicate_offset = crate::Token2022ProofOrchestrationRequest {
operation_code: "operation".to_string(),
proofs: vec![
ks_lib::ExSplTokenConfidentialProofReference {
kind: ks_lib::ExSplTokenConfidentialProofKind::ZeroCiphertext,
location: ks_lib::ExSplTokenConfidentialProofLocation::InstructionOffset {
offset: 1,
},
},
ks_lib::ExSplTokenConfidentialProofReference {
kind: ks_lib::ExSplTokenConfidentialProofKind::BatchedRangeProofU64,
location: ks_lib::ExSplTokenConfidentialProofLocation::InstructionOffset {
offset: 1,
},
},
],
expected_context_authority: std::option::Option::None,
compute_unit_limit: 1,
max_fee_lamports: 1,
submit: false,
operator_confirmed: false,
};
let empty = crate::Token2022CryptographicPreflightReport {
commitment: "confirmed".to_string(),
context_slot: 0,
proof_contexts: vec![],
};
assert!(crate::orchestrate_token_2022_proofs(&duplicate_offset, &empty).is_err());
let mut unconfirmed = duplicate_offset;
unconfirmed.proofs.clear();
unconfirmed.submit = true;
assert!(crate::orchestrate_token_2022_proofs(&unconfirmed, &empty).is_err());
}
}

View File

@@ -0,0 +1,751 @@
// file: ks-pipeline/src/spl_token_2022_stateful.rs
// version: 9
//! 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_TOKEN_2022_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: ks_lib::MdPubkey,
/// Expected Token-2022 base-state category.
pub kind: ks_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_token_2022_stateful_snapshot(
pool: &ks_onchain_transport::HttpEndpointPool,
request: &crate::Token2022StatefulReadRequest,
) -> ks_core::Result<crate::Token2022StatefulReadResult> {
if request.query_role.trim().is_empty() {
return std::result::Result::Err(ks_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_TOKEN_2022_STATEFUL_ACCOUNT_BYTES
{
return std::result::Result::Err(ks_core::Error::config(format!(
"Token-2022 stateful read max_data_bytes must be between 1 and {}",
crate::MAX_TOKEN_2022_STATEFUL_ACCOUNT_BYTES
)));
}
let config = match ks_onchain_transport::GetAccountInfoConfig::new_with_data(
ks_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_token_2022_account_info_result(request, &result);
}
/// Validates one complete RPC account response before Token-2022 parsing and routing.
pub fn materialize_token_2022_account_info_result(
request: &crate::Token2022StatefulReadRequest,
result: &ks_onchain_transport::AccountInfoResult,
) -> ks_core::Result<crate::Token2022StatefulReadResult> {
if request.max_data_bytes == 0
|| request.max_data_bytes > crate::MAX_TOKEN_2022_STATEFUL_ACCOUNT_BYTES
{
return std::result::Result::Err(ks_core::Error::config(format!(
"Token-2022 stateful read max_data_bytes must be between 1 and {}",
crate::MAX_TOKEN_2022_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(ks_core::Error::new(
"token_2022_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(ks_core::Error::new(
"token_2022_stateful_account_missing",
format!("Token-2022 account {} does not exist", request.account.0),
));
},
};
if account.executable {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_stateful_account_executable",
format!("Token-2022 state account {} must not be executable", request.account.0),
));
}
if account.owner.0 != ks_program_ids::SPL_TOKEN_2022_PROGRAM_ID {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_stateful_owner_mismatch",
format!(
"Token-2022 state account {} owner must be {}, got {}",
request.account.0,
ks_program_ids::SPL_TOKEN_2022_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(ks_core::Error::new(
"token_2022_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(ks_core::Error::new(
"token_2022_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 ks_lib::decoder_spl_token_2022_parse_token_2022_state(
request.kind,
account.data.as_slice(),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_stateful_parse_failed",
error,
));
},
};
let snapshot = match crate::materialize_parsed_token_2022_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(ks_core::Error::new(
"token_2022_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<ks_lib::MtApiMaterializedOutput>,
}
/// Parse, contextually validate, and materialize one bounded Token-2022 account snapshot.
pub fn materialize_token_2022_stateful_snapshot(
account_key: &str,
owner_program_id: &str,
slot: u64,
kind: ks_lib::DcToken2022StateKind,
data: &[u8],
) -> std::result::Result<crate::Token2022StatefulSnapshotBundle, String> {
if owner_program_id != ks_program_ids::SPL_TOKEN_2022_PROGRAM_ID {
return std::result::Result::Err(format!(
"Token-2022 state snapshot owner must be {}, got {owner_program_id}",
ks_program_ids::SPL_TOKEN_2022_PROGRAM_ID
));
}
let state = match ks_lib::decoder_spl_token_2022_parse_token_2022_state(kind, data) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return crate::materialize_parsed_token_2022_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_token_2022_stateful_snapshot(
account_key: &str,
slot: u64,
state: &ks_lib::DcToken2022State,
) -> std::result::Result<crate::Token2022StatefulSnapshotBundle, String> {
return crate::materialize_parsed_token_2022_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_token_2022_stateful_snapshot_with_context(
account_key: &str,
slot: u64,
state: &ks_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 ks_lib::materializer_token_materialize_token_2022_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 == ks_lib::DcToken2022StateKind::Mint {
let metadata_outputs = match ks_lib::materializer_metadata_materialize_token_2022_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 ks_lib::materializer_fees_materialize_token_2022_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 ks_lib::materializer_admin_materialize_token_2022_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 {
ks_lib::DcToken2022StateKind::Mint => "mint",
ks_lib::DcToken2022StateKind::Account => "account",
ks_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: &ks_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: &ks_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) -> ks_lib::DcToken2022State {
return ks_lib::DcToken2022State {
kind: ks_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![
ks_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"
}),
},
ks_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_token_2022_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_token_2022_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_token_2022_stateful_snapshot(
account_key.as_str(),
ks_program_ids::SPL_TOKEN_PROGRAM_ID,
1,
ks_lib::DcToken2022StateKind::Mint,
&data,
);
assert!(wrong_owner.is_err());
let state = mint_state(account_key.as_str());
let malformed_key =
crate::materialize_parsed_token_2022_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 = ks_lib::DcToken2022State {
kind: ks_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_token_2022_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 = ks_lib::DcToken2022State {
kind: ks_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![ks_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_token_2022_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_token_2022_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_token_2022_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 = ks_lib::DcToken2022State {
kind: ks_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![ks_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_token_2022_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!("materializer.fees"))
);
let account_address = account_key(16);
let account = ks_lib::DcToken2022State {
kind: ks_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![ks_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_token_2022_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(ks_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_token_2022_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!("token_2022_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: ks_lib::MdPubkey(account.clone()),
kind: ks_lib::DcToken2022StateKind::Mint,
min_context_slot: std::option::Option::Some(40),
max_data_bytes: 82,
context: crate::Token2022StatefulContext::default(),
};
let result = ks_onchain_transport::AccountInfoResult {
context: ks_onchain_transport::RpcResponseContext {
slot: 41,
api_version: std::option::Option::None,
},
account: std::option::Option::Some(ks_onchain_transport::AccountInfoValue {
lamports: 1,
owner: ks_lib::MdProgramId(ks_program_ids::SPL_TOKEN_2022_PROGRAM_ID.to_string()),
executable: false,
rent_epoch: 0,
space: 82,
data: std::vec![0; 82],
}),
};
let materialized = crate::materialize_token_2022_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: ks_lib::MdPubkey(account),
kind: ks_lib::DcToken2022StateKind::Mint,
min_context_slot: std::option::Option::Some(50),
max_data_bytes: 82,
context: crate::Token2022StatefulContext::default(),
};
let base = ks_onchain_transport::AccountInfoValue {
lamports: 1,
owner: ks_lib::MdProgramId(ks_program_ids::SPL_TOKEN_2022_PROGRAM_ID.to_string()),
executable: false,
rent_epoch: 0,
space: 82,
data: std::vec![0; 82],
};
let stale = ks_onchain_transport::AccountInfoResult {
context: ks_onchain_transport::RpcResponseContext {
slot: 49,
api_version: std::option::Option::None,
},
account: std::option::Option::Some(base.clone()),
};
assert!(crate::materialize_token_2022_account_info_result(&request, &stale).is_err());
let mut foreign = base.clone();
foreign.owner = ks_lib::MdProgramId(ks_program_ids::SPL_TOKEN_PROGRAM_ID.to_string());
let foreign = ks_onchain_transport::AccountInfoResult {
context: ks_onchain_transport::RpcResponseContext {
slot: 50,
api_version: std::option::Option::None,
},
account: std::option::Option::Some(foreign),
};
assert!(crate::materialize_token_2022_account_info_result(&request, &foreign).is_err());
let mut executable = base.clone();
executable.executable = true;
let executable = ks_onchain_transport::AccountInfoResult {
context: ks_onchain_transport::RpcResponseContext {
slot: 50,
api_version: std::option::Option::None,
},
account: std::option::Option::Some(executable),
};
assert!(crate::materialize_token_2022_account_info_result(&request, &executable).is_err());
let mut incomplete = base;
incomplete.data.pop();
let incomplete = ks_onchain_transport::AccountInfoResult {
context: ks_onchain_transport::RpcResponseContext {
slot: 50,
api_version: std::option::Option::None,
},
account: std::option::Option::Some(incomplete),
};
assert!(crate::materialize_token_2022_account_info_result(&request, &incomplete).is_err());
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,24 @@
// file: ks-pipeline/tests/external_metadata_metaplex_token_metadata_pipeline_api.rs
// version: 2
//! External-only coverage for the public Metaplex Token Metadata pipeline contract.
#[test]
fn external_consumers_can_request_edition_marker_state_through_crate_root_exports() {
let account = ks_lib::MdPubkey(solana_pubkey::Pubkey::new_from_array([7_u8; 32]).to_string());
let mint = ks_lib::MdPubkey(solana_pubkey::Pubkey::new_from_array([8_u8; 32]).to_string());
let request = ks_pipeline::MetaplexTokenMetadataStatefulReadRequest {
query_role: "rpc".to_string(),
account,
kind: ks_pipeline::MetaplexTokenMetadataAccountKind::EditionMarker { mint, edition: 1 },
min_context_slot: std::option::Option::Some(42),
max_data_bytes: ks_pipeline::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES,
};
assert!(request.validate().is_ok());
let json = match serde_json::to_value(&request) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("request serialization failed: {error}"),
};
assert_eq!(json["kind"]["kind"], "edition_marker");
assert_eq!(json["kind"]["edition"], 1);
}

View File

@@ -0,0 +1,31 @@
// file: ks-pipeline/tests/external_metadata_solana_program_pipeline_api.rs
// version: 3
//! External-only coverage for the public Solana Program Metadata pipeline contract.
#[test]
fn external_consumers_use_only_public_program_metadata_pipeline_exports() {
let account = ks_lib::MdPubkey(solana_pubkey::Pubkey::new_from_array([7_u8; 32]).to_string());
let request = ks_pipeline::SolanaProgramMetadataStatefulReadRequest {
query_role: "rpc".to_string(),
account: account.clone(),
expected_state: ks_pipeline::SolanaProgramMetadataExpectedAccountState::Any,
min_context_slot: std::option::Option::None,
max_data_bytes: ks_pipeline::MAX_SOLANA_PROGRAM_METADATA_STATEFUL_ACCOUNT_BYTES,
};
assert!(request.validate().is_ok());
let postcondition = ks_pipeline::SolanaProgramMetadataPostcondition {
code: "metadata.solana_program_metadata.external_api".to_string(),
account,
status: ks_pipeline::SolanaProgramMetadataPostconditionStatus::NotApplicable,
diagnostic: "external API coverage".to_string(),
};
assert_eq!(
ks_pipeline::summarize_solana_program_metadata_postconditions(&[postcondition]),
ks_pipeline::SolanaProgramMetadataPostconditionStatus::NotApplicable
);
const {
assert!(ks_pipeline::MAX_SOLANA_PROGRAM_METADATA_EXECUTION_SIGNERS > 0);
assert!(ks_pipeline::MAX_SOLANA_PROGRAM_METADATA_PREFLIGHT_ACCOUNTS > 0);
}
}

View File

@@ -0,0 +1,42 @@
// file: ks-pipeline/tests/external_metadata_token_2022_pipeline_api.rs
// version: 2
//! External-only coverage for the public Token-2022 Token Metadata pipeline contract.
#[test]
fn external_consumers_use_only_public_token_2022_metadata_pipeline_exports() {
let account = ks_lib::MdPubkey(solana_pubkey::Pubkey::new_from_array([9_u8; 32]).to_string());
let request = ks_pipeline::Token2022StatefulReadRequest {
query_role: "rpc".to_string(),
account: account.clone(),
kind: ks_lib::DcToken2022StateKind::Mint,
min_context_slot: std::option::Option::Some(42),
max_data_bytes: ks_pipeline::MAX_TOKEN_2022_STATEFUL_ACCOUNT_BYTES,
context: ks_pipeline::Token2022StatefulContext::default(),
};
assert_eq!(request.account, account);
assert_eq!(request.kind, ks_lib::DcToken2022StateKind::Mint);
assert!(
ks_pipeline::validate_token_2022_metadata_emit_range(
std::option::Option::Some(0),
std::option::Option::Some(32),
)
.is_ok()
);
let _emit_inspector: fn(
&ks_lib::ExApiExecutionSimulationResult,
std::option::Option<u64>,
std::option::Option<u64>,
) -> ks_core::Result<ks_pipeline::Token2022MetadataEmitEvidence> =
ks_pipeline::inspect_token_2022_metadata_emit_simulation;
let _authority_inspector: fn(
&ks_lib::MdPubkey,
std::option::Option<&ks_lib::MdPubkey>,
&ks_pipeline::Token2022StatefulSnapshotBundle,
) -> ks_pipeline::Token2022ExecutionPostcondition =
ks_pipeline::inspect_token_2022_metadata_authority_postcondition;
const {
assert!(ks_pipeline::MAX_TOKEN_2022_METADATA_EMIT_BYTES > 0);
assert!(ks_pipeline::MAX_TOKEN_2022_STATEFUL_ACCOUNT_BYTES > 0);
}
}