diff --git a/Cargo.toml b/Cargo.toml index 6911d7e..a45c197 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ # file: Cargo.toml -# version: 29 +# version: 30 [workspace] resolver = "3" @@ -18,7 +18,7 @@ members = [ ] [workspace.package] -version = "0.4.8-pre.7" +version = "0.4.8-pre.8" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-bot3" diff --git a/docs/README.md b/docs/README.md index 5ce349a..04c5b63 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,5 +1,5 @@ - + # Documentation active de Khadhroony Bot3 @@ -42,6 +42,9 @@ Modèles documentaires non génératifs : - [`audits/V0_4_8_PRE_002_METADATA_CONTRACT_AUDIT.md`](audits/V0_4_8_PRE_002_METADATA_CONTRACT_AUDIT.md) ; - [`audits/V0_4_8_PRE_007_METAPLEX_MATERIALIZER_STRUCTURE_AUDIT.md`](audits/V0_4_8_PRE_007_METAPLEX_MATERIALIZER_STRUCTURE_AUDIT.md) ; - [`audits/V0_4_8_PRE_007_MATERIALIZER_CONVENTION_AUDIT.md`](audits/V0_4_8_PRE_007_MATERIALIZER_CONVENTION_AUDIT.md) ; +- [`audits/V0_4_8_PRE_008_METADATA_SOLANA_PROGRAM_PIPELINE_AUDIT.md`](audits/V0_4_8_PRE_008_METADATA_SOLANA_PROGRAM_PIPELINE_AUDIT.md) ; +- [`audits/V0_4_8_PRE_008_METADATA_AND_PIPELINE_NOMENCLATURE_AUDIT.md`](audits/V0_4_8_PRE_008_METADATA_AND_PIPELINE_NOMENCLATURE_AUDIT.md) ; +- [`audits/V0_4_8_PRE_008_SOLANA_PROGRAM_METADATA_HISTORICAL_REPLAY_FIX.md`](audits/V0_4_8_PRE_008_SOLANA_PROGRAM_METADATA_HISTORICAL_REPLAY_FIX.md) ; - [`decisions/DOCUMENT_ARCHIVE_SELECTION_POLICY.md`](decisions/DOCUMENT_ARCHIVE_SELECTION_POLICY.md) ; - [`decisions/WINCODE_COMPATIBILITY_POLICY.md`](decisions/WINCODE_COMPATIBILITY_POLICY.md). @@ -101,7 +104,9 @@ Les onze crates possèdent désormais `README.md`, `TODO.md`, `USAGE.md` et `CHA - [`Audit contractuel 0.4.8-pre.002 — metadata`](audits/V0_4_8_PRE_002_METADATA_CONTRACT_AUDIT.md) ; - [`Audit 0.4.8-pre.005 — instructions et historique Solana Program Metadata`](audits/V0_4_8_PRE_005_SOLANA_PROGRAM_METADATA_INSTRUCTION_HISTORY_AUDIT.md) ; - [`Audit 0.4.8-pre.007 — structure du matérialiseur Metaplex Token Metadata`](audits/V0_4_8_PRE_007_METAPLEX_MATERIALIZER_STRUCTURE_AUDIT.md) ; -- [`Audit 0.4.8-pre.007 — conventions et structure de tous les matérialisateurs`](audits/V0_4_8_PRE_007_MATERIALIZER_CONVENTION_AUDIT.md). +- [`Audit 0.4.8-pre.007 — conventions et structure de tous les matérialisateurs`](audits/V0_4_8_PRE_007_MATERIALIZER_CONVENTION_AUDIT.md) ; +- [`Audit 0.4.8-pre.008 — pipeline Solana Program Metadata`](audits/V0_4_8_PRE_008_METADATA_SOLANA_PROGRAM_PIPELINE_AUDIT.md). +- [`Correctif 0.4.8-pre.008 — replay historique Solana Program Metadata`](audits/V0_4_8_PRE_008_SOLANA_PROGRAM_METADATA_HISTORICAL_REPLAY_FIX.md). ## Plans de version actifs diff --git a/docs/audits/V0_4_8_PRE_007_MATERIALIZER_CONVENTION_AUDIT.md b/docs/audits/V0_4_8_PRE_007_MATERIALIZER_CONVENTION_AUDIT.md index 47fc975..576a383 100644 --- a/docs/audits/V0_4_8_PRE_007_MATERIALIZER_CONVENTION_AUDIT.md +++ b/docs/audits/V0_4_8_PRE_007_MATERIALIZER_CONVENTION_AUDIT.md @@ -1,5 +1,5 @@ - + # Audit des conventions et de la structure des matérialisateurs — `0.4.8-pre.007-fix-004` @@ -45,37 +45,35 @@ Les identités runtime servent au code et au routage des logs. Les `processorNam ## 3. Inventaire audité -| Composant | Type principal ou API | Statut | Identité runtime | `processorName` | -|-------------------------|---------------------------------------|------------|--------------------------------------------------------|-------------------------------------------------| -| Administration | `MtAdminMaterializer` | actif | `kb-lib.materializer.admin` | `materializer.admin` | -| Bridge | `MtBridgeMaterializer` | réservé | `kb-lib.materializer.bridge` | `materializer.bridge` | -| Compliance audit | `MtComplianceAuditMaterializer` | actif | `kb-lib.materializer.compliance.audit` | `materializer.compliance.audit` | -| Fees | `MtFeesMaterializer` | actif | `kb-lib.materializer.fees` | `materializer.fees` | -| Governance | `MtGovernanceMaterializer` | réservé | `kb-lib.materializer.governance` | `materializer.governance` | -| Lending | `MtLendingMaterializer` | réservé | `kb-lib.materializer.lending` | `materializer.lending` | -| Lifecycle | `MtLifecycleMaterializer` | actif | `kb-lib.materializer.lifecycle` | `materializer.lifecycle` | -| Liquidity | `MtLiquidityMaterializer` | réservé | `kb-lib.materializer.liquidity` | `materializer.liquidity` | -| Metaplex Token Metadata | APIs de snapshots | state-only | `kb-lib.materializer.metadata.metaplex_token_metadata` | `materializer.metadata.metaplex_token_metadata` | -| Solana Program Metadata | `MtSolanaProgramMetadataMaterializer` | actif | `kb-lib.materializer.metadata.solana_program_metadata` | `materializer.metadata.solana_program_metadata` | -| Token-2022 metadata | API de snapshots | state-only | `kb-lib.materializer.metadata.token_2022` | `materializer.metadata.token_2022` | -| NFT | `MtNftMaterializer` | réservé | `kb-lib.materializer.nft` | `materializer.nft` | -| Oracle | `MtOracleMaterializer` | réservé | `kb-lib.materializer.oracle` | `materializer.oracle` | -| Orderbook | `MtOrderbookMaterializer` | réservé | `kb-lib.materializer.orderbook` | `materializer.orderbook` | -| Perpetuals | `MtPerpetualsMaterializer` | réservé | `kb-lib.materializer.perpetuals` | `materializer.perpetuals` | -| Pool state | `MtPoolStateMaterializer` | réservé | `kb-lib.materializer.pool.state` | `materializer.pool.state` | -| Rewards | `MtRewardsMaterializer` | réservé | `kb-lib.materializer.rewards` | `materializer.rewards` | -| Risk | `MtRiskMaterializer` | actif | `kb-lib.materializer.risk` | `materializer.risk` | -| Routing | `MtRoutingMaterializer` | réservé | `kb-lib.materializer.routing` | `materializer.routing` | -| Staking | `MtStakingMaterializer` | actif | `kb-lib.materializer.staking` | `materializer.staking` | -| Token accounts | `MtTokenAccountsMaterializer` | actif | `kb-lib.materializer.token.accounts` | `materializer.token.accounts` | -| Token metadata risk | `MtTokenMetadataRiskMaterializer` | réservé | `kb-lib.materializer.token.metadata_risk` | `materializer.token.metadata_risk` | -| Trades | `MtTradesMaterializer` | réservé | `kb-lib.materializer.trades` | `materializer.trades` | -| Transaction annotations | `MtTransactionAnnotationMaterializer` | actif | `kb-lib.materializer.transaction.annotations` | `materializer.transaction.annotations` | -| Vault | `MtVaultMaterializer` | réservé | `kb-lib.materializer.vault` | `materializer.vault` | +| Composant | Type principal ou API | Statut | Identité runtime | `processorName` | +|-------------------------|-----------------------------------------------|---------|--------------------------------------------------------|-------------------------------------------------| +| Administration | `MtAdminMaterializer` | actif | `kb-lib.materializer.admin` | `materializer.admin` | +| Bridge | `MtBridgeMaterializer` | réservé | `kb-lib.materializer.bridge` | `materializer.bridge` | +| Compliance audit | `MtComplianceAuditMaterializer` | actif | `kb-lib.materializer.compliance.audit` | `materializer.compliance.audit` | +| Fees | `MtFeesMaterializer` | actif | `kb-lib.materializer.fees` | `materializer.fees` | +| Governance | `MtGovernanceMaterializer` | réservé | `kb-lib.materializer.governance` | `materializer.governance` | +| Lending | `MtLendingMaterializer` | réservé | `kb-lib.materializer.lending` | `materializer.lending` | +| Lifecycle | `MtLifecycleMaterializer` | actif | `kb-lib.materializer.lifecycle` | `materializer.lifecycle` | +| Liquidity | `MtLiquidityMaterializer` | réservé | `kb-lib.materializer.liquidity` | `materializer.liquidity` | +| Metaplex Token Metadata | `MtMetadataMetaplexTokenMetadataMaterializer` | actif | `kb-lib.materializer.metadata.metaplex_token_metadata` | `materializer.metadata.metaplex_token_metadata` | +| Solana Program Metadata | `MtMetadataSolanaProgramMaterializer` | actif | `kb-lib.materializer.metadata.solana_program_metadata` | `materializer.metadata.solana_program_metadata` | +| Token-2022 metadata | `MtMetadataToken2022Materializer` | actif | `kb-lib.materializer.metadata.token_2022` | `materializer.metadata.token_2022` | +| NFT | `MtNftMaterializer` | réservé | `kb-lib.materializer.nft` | `materializer.nft` | +| Oracle | `MtOracleMaterializer` | réservé | `kb-lib.materializer.oracle` | `materializer.oracle` | +| Orderbook | `MtOrderbookMaterializer` | réservé | `kb-lib.materializer.orderbook` | `materializer.orderbook` | +| Perpetuals | `MtPerpetualsMaterializer` | réservé | `kb-lib.materializer.perpetuals` | `materializer.perpetuals` | +| Pool state | `MtPoolStateMaterializer` | réservé | `kb-lib.materializer.pool.state` | `materializer.pool.state` | +| Rewards | `MtRewardsMaterializer` | réservé | `kb-lib.materializer.rewards` | `materializer.rewards` | +| Risk | `MtRiskMaterializer` | actif | `kb-lib.materializer.risk` | `materializer.risk` | +| Routing | `MtRoutingMaterializer` | réservé | `kb-lib.materializer.routing` | `materializer.routing` | +| Staking | `MtStakingMaterializer` | actif | `kb-lib.materializer.staking` | `materializer.staking` | +| Token accounts | `MtTokenAccountsMaterializer` | actif | `kb-lib.materializer.token.accounts` | `materializer.token.accounts` | +| Token metadata risk | `MtTokenMetadataRiskMaterializer` | réservé | `kb-lib.materializer.token.metadata_risk` | `materializer.token.metadata_risk` | +| Trades | `MtTradesMaterializer` | réservé | `kb-lib.materializer.trades` | `materializer.trades` | +| Transaction annotations | `MtTransactionAnnotationMaterializer` | actif | `kb-lib.materializer.transaction.annotations` | `materializer.transaction.annotations` | +| Vault | `MtVaultMaterializer` | réservé | `kb-lib.materializer.vault` | `materializer.vault` | -Bilan : **25 composants nommés**, dont **9 matérialisateurs actifs**, **14 coquilles réservées** et **2 composants state-only**. - -`MtMetadataMaterializer` reste un wrapper public de compatibilité vers `MtSolanaProgramMetadataMaterializer`. Il ne possède pas une identité distincte et ne crée pas un second propriétaire de projection. +Bilan après correction `pre.008` : **25 composants nommés**, dont **11 matérialisateurs actifs** et **14 coquilles réservées**. Les trois domaines metadata exposent des types spécialisés ; aucun wrapper metadata générique ne masque leur ownership. ## 4. Structure normalisée @@ -161,7 +159,7 @@ Les 22 chemins retirés sont fournis dans `delete-files.txt` à la racine du del ## 10. Conclusion -Les matérialisateurs utilisent désormais une convention unique pour les identités runtime, les processeurs persistés, les tracing targets, les constantes, les façades et les coquilles réservées. Les différences restantes sont fonctionnelles et explicites : un composant peut être actif, réservé ou state-only, mais il ne change pas de convention structurelle ou de provenance selon son statut. +Les matérialisateurs utilisent désormais une convention unique pour les identités runtime, les processeurs persistés, les tracing targets, les constantes, les façades et les coquilles réservées. Un composant peut être actif ou réservé ; les APIs stateful restent des capacités de son matérialiseur spécialisé et ne constituent pas un type générique parallèle. ## 11. Correction des identités opérationnelles et du tracing @@ -171,7 +169,7 @@ La compilation du workspace corrigé a révélé trois tracing targets déclaré - `kb-lib.materializer.metadata.metaplex_token_metadata` ; - `kb-lib.materializer.metadata.token_2022`. -Le composant state-only Token-2022 réexportait également `MT_METADATA_TOKEN_2022_COMPONENT_NAME` sans le consommer dans son implémentation. Cette situation ne signifiait pas que les snapshots Token-2022 n’étaient pas matérialisés : la fonction `materializer_metadata_materialize_token_2022_snapshot` produisait déjà les projections `TokenMetadata`, `TokenGroup` et `TokenGroupMember`. Elle révélait en revanche que l’identité runtime du composant n’était pas attachée à une opération observable. +Le composant de snapshots Token-2022 réexportait également `MT_METADATA_TOKEN_2022_COMPONENT_NAME` sans le consommer dans son implémentation. Cette situation ne signifiait pas que les snapshots Token-2022 n’étaient pas matérialisés : la fonction `materializer_metadata_materialize_token_2022_snapshot` produisait déjà les projections `TokenMetadata`, `TokenGroup` et `TokenGroupMember`. Elle révélait en revanche que l’identité runtime du composant n’était pas attachée à une opération observable. Le correctif ajoute des événements structurés réels pour les chemins suivants : @@ -186,4 +184,4 @@ L’audit mécanique refuse désormais : - un `TRACING_TARGET_*` de `kb-lib` déclaré sans macro `tracing` réelle ; - un `MT_*_COMPONENT_NAME` qui n’est consommé par aucun fichier d’implémentation du composant. -Les composants state-only restent distincts des matérialiseurs instructionnels : ils n’implémentent pas artificiellement `MtApiEventMaterializer`, mais leur identité runtime est désormais effectivement utilisée par leurs opérations de snapshot. +La correction `pre.008` ajoute les matérialiseurs instructionnels spécialisés Metaplex et Token-2022 parce que leurs instructions sont effectivement décodées. Leurs APIs de snapshots restent distinctes et autoritatives, tandis que les sorties d’instructions restent des faits committés non autoritatifs. diff --git a/docs/audits/V0_4_8_PRE_008_METADATA_AND_PIPELINE_NOMENCLATURE_AUDIT.md b/docs/audits/V0_4_8_PRE_008_METADATA_AND_PIPELINE_NOMENCLATURE_AUDIT.md new file mode 100644 index 0000000..56905ec --- /dev/null +++ b/docs/audits/V0_4_8_PRE_008_METADATA_AND_PIPELINE_NOMENCLATURE_AUDIT.md @@ -0,0 +1,52 @@ + + + +# Audit de nomenclature metadata et pipeline + +## 1. Objet + +Cet audit corrige deux dérives révélées après l’intégration pipeline de `0.4.8-pre.008` : un matérialiseur Solana Program Metadata nommé hors de la hiérarchie `MtMetadata*`, et des modules `kb-pipeline` préfixés uniformément par `solana_` malgré des domaines Metaplex ou SPL distincts. + +## 2. Matérialiseurs metadata + +La surface publique canonique comprend désormais exactement trois types spécialisés : + +| Domaine | Type | Processor | +|---------------------------|-----------------------------------------------|-------------------------------------------------| +| Metaplex Token Metadata | `MtMetadataMetaplexTokenMetadataMaterializer` | `materializer.metadata.metaplex_token_metadata` | +| Solana Program Metadata | `MtMetadataSolanaProgramMaterializer` | `materializer.metadata.solana_program_metadata` | +| Token-2022 metadata/group | `MtMetadataToken2022Materializer` | `materializer.metadata.token_2022` | + +Le type `MtSolanaProgramMetadataMaterializer` et le wrapper `MtMetadataMaterializer` sont supprimés. Chaque type implémente les contrats legacy et contextuel, possède ses constantes, son tracing target et son ownership instructionnel. Les helpers `state.rs` et `account.rs` restent séparés : ils produisent les snapshots autoritatifs, alors que les matérialiseurs d’instructions produisent des faits committés non autoritatifs. + +Les neuf instructions incorporées Token Metadata et Token Group sont classées sous `MdEventFamily::Metadata`. Cette classification évite de confier les changements d’autorité propres aux interfaces metadata au matérialiseur d’administration générique. + +## 3. Registre desktop + +`demo_decode_replay` enregistre les trois types spécialisés. Un test construit une observation représentative de chaque domaine et vérifie qu’elle possède exactement un propriétaire dans le registre runtime. + +## 4. Nomenclature `kb-pipeline` + +Les modules privés suivent la hiérarchie suivante : + +- `metadata_metaplex_token_metadata_*` ; +- `metadata_solana_program_*` ; +- `spl_ata_stateful` ; +- `spl_elgamal_registry_stateful` ; +- `spl_token_stateful` ; +- `spl_token_2022_*` ; +- `solana_stateful` uniquement pour les contrats Solana Core transversaux. + +Les fonctions et structures publiques restent inchangées. Le renommage est donc structurel et documentaire, sans rupture volontaire de l’API consommée par le desktop ou les futurs scénarios Devnet. + +## 5. Fichiers remplacés + +Les anciens fichiers `solana_*` de `kb-pipeline`, l’ancien test externe et l’ancienne matrice sont listés dans `delete-files.txt`. Leur suppression est obligatoire après extraction du delta. + +## 6. Garde-fous + +- les exports racine couvrent les trois matérialiseurs spécialisés ; +- le test externe n’utilise que la façade `kb_lib::*` ; +- les familles Token-2022 metadata/group sont testées exhaustivement ; +- le test externe pipeline utilise des assertions de compilation `const` ; +- l’audit des règles vérifie les façades, les exports, les constantes et la nomenclature des composants. diff --git a/docs/audits/V0_4_8_PRE_008_METADATA_SOLANA_PROGRAM_PIPELINE_AUDIT.md b/docs/audits/V0_4_8_PRE_008_METADATA_SOLANA_PROGRAM_PIPELINE_AUDIT.md new file mode 100644 index 0000000..0a931cc --- /dev/null +++ b/docs/audits/V0_4_8_PRE_008_METADATA_SOLANA_PROGRAM_PIPELINE_AUDIT.md @@ -0,0 +1,113 @@ + + + +# Audit du pipeline Solana Program Metadata — `0.4.8-pre.008` + +## 1. Objet + +Cet audit ferme la frontière réutilisable de Solana Program Metadata dans `kb-pipeline` sans introduire de fixture, de wallet de démonstration ni de dépendance envers `kb-pipeline-demo-scenarios`. + +La surface couverte est le programme : + +```text +ProgM6JCCvbYkfKqJYHePx4xxSUSqJp7rh8Lyv7nk7S +``` + +## 2. Séparation des responsabilités + +| Crate ou composant | Responsabilité en `pre.008` | +|-----------------------------------------------|----------------------------------------------------------------------------------------| +| `kb-lib` | modèles, décodage, matérialisation, intents, builders, exécuteur et safety commun | +| `kb-pipeline` | lectures stateful, préflight, readiness simulation-first et postconditions | +| `kb-pipeline-demo-scenarios` | aucune modification ; fixture et parcours Devnet réservés à `pre.009` | +| `kb-app-demo-desktop/demo_decode_replay` | enregistrement générique du décodeur et du matérialiseur pour la validation historique | +| `kb-app-demo-desktop/demo_execution_metadata` | aucune modification ; intégration `ProgM6…` réservée à `pre.009` | + +## 3. Lectures stateful + +Le pipeline distingue explicitement : + +- compte absent ; +- compte présent mais vide ou intégralement nul ; +- compte `Buffer` initialisé ; +- compte `Metadata` initialisé. + +Les réponses RPC doivent être confirmées, non exécutables, complètes et sous la borne maximale. Un état initialisé doit être détenu par `ProgM6…`. Le décodage et la projection autoritative sont délégués aux APIs publiques de `kb-lib`. + +## 4. Préflight + +Le préflight lie l’intent typé au plan préparé et contrôle : + +- le Program ID et le code d’opération exacts ; +- l’unique instruction du plan ; +- l’approbation explicite des mutations sensibles ; +- la décision de `ExSafetyChecker` ; +- les snapshots confirmés et non dupliqués ; +- l’état du compte cible ; +- les sources `Buffer` ; +- l’autorité explicite ou le contexte canonical ; +- le préfinancement rent des opérations d’allocation ou d’agrandissement. + +Une décision `Deny` bloque le traitement. Une décision `RequireConfirmation` reste exécutable mais doit être satisfaite dans l’enveloppe de readiness. + +## 5. Readiness et postconditions + +La readiness refuse : + +- un plan non lié au préflight ; +- un message non simulé ou simulé sous un autre hash ; +- une simulation en échec ; +- un signer requis non résolu ; +- une soumission sans confirmation opérateur ; +- une soumission depuis un plan `dry_run`. + +Les neuf opérations possèdent une postcondition stateful : + +| Opération | Postcondition principale | +|----------------|-----------------------------------------------------------------| +| `Write` | octets attendus au bon offset du `Buffer` | +| `Initialize` | état `Metadata`, PDA, seed, descripteurs et contenu compatibles | +| `SetAuthority` | autorité finale exacte ou supprimée | +| `SetData` | descripteurs et contenu final compatibles | +| `SetImmutable` | `mutable = false` | +| `Trim` | même type d’état et taille non croissante | +| `Close` | compte absent | +| `Allocate` | état `Buffer`, autorité, seed et canonicité compatibles | +| `Extend` | même type d’état et taille augmentée exactement | + +Une lecture finale absente du rapport produit `NotApplicable`, jamais `Confirmed`. + +## 6. Validation historique prévue + +La validation historique opérateur suit : + +1. `demo_backfill` sur un échantillon borné de signatures ; +2. extraction Core ; +3. decode replay ciblé sur `ProgM6…` et `metadata.solana_program_metadata` ; +4. matérialisation via `materializer.metadata.solana_program_metadata` ; +5. lecture bornée des événements matérialisés. + +Cette validation précède les scénarios Devnet de `pre.009` et ne dépend pas de leur fixture. + +## 7. Matrice + +La matrice machine-readable est : + +```text +test-fixtures/contract-matrices/METADATA_SOLANA_PROGRAM_PIPELINE_MATRIX.json +``` + +Elle distingue explicitement la crate généraliste du package de scénarios et ferme l’inventaire des neuf opérations. +## 8. Correction de nomenclature `pre.008` + +Les modules privés de `kb-pipeline` sont renommés selon leur domaine : + +- `metadata_metaplex_token_metadata_*` pour Metaplex Token Metadata ; +- `metadata_solana_program_*` pour Solana Program Metadata ; +- `spl_ata_stateful` et `spl_elgamal_registry_stateful` pour les programmes SPL correspondants ; +- `spl_token_stateful` et `spl_token_2022_*` pour SPL Token et Token-2022 ; +- `solana_stateful` reste réservé aux primitives Solana Core transversales. + +Les fonctions et structures publiques restent stables. La matrice et le test externe suivent la même nomenclature de fichier. + +Le registre de replay desktop utilise désormais trois matérialiseurs metadata spécialisés : `MtMetadataMetaplexTokenMetadataMaterializer`, `MtMetadataSolanaProgramMaterializer` et `MtMetadataToken2022Materializer`. Aucun wrapper générique `MtMetadataMaterializer` n’est conservé. diff --git a/docs/audits/V0_4_8_PRE_008_SOLANA_PROGRAM_METADATA_HISTORICAL_REPLAY_FIX.md b/docs/audits/V0_4_8_PRE_008_SOLANA_PROGRAM_METADATA_HISTORICAL_REPLAY_FIX.md new file mode 100644 index 0000000..e7f017e --- /dev/null +++ b/docs/audits/V0_4_8_PRE_008_SOLANA_PROGRAM_METADATA_HISTORICAL_REPLAY_FIX.md @@ -0,0 +1,54 @@ + + + +# Correctif du replay historique Solana Program Metadata + +## 1. Validation ayant révélé le défaut + +La validation Mainnet a importé deux pages de 100 transactions du programme `ProgM6JCCvbYkfKqJYHePx4xxSUSqJp7rh8Lyv7nk7S`, puis extrait 200 transactions et rejoué 582 instructions. Le processeur `metadata.solana_program_metadata` a reconnu et dispatché 132 instructions, mais les 132 ont terminé en échec de décodage. + +Les surfaces observées étaient : + +| Instruction | Observations | +|-----------------|-------------:| +| `write` | 88 | +| `allocate` | 15 | +| `initialize` | 11 | +| `set_authority` | 7 | +| `close` | 5 | +| `set_data` | 5 | +| `set_immutable` | 1 | +| `trim` | 0 | +| `extend` | 0 | + +Le fait que toutes les instructions observées échouent malgré des discriminants reconnus et des transactions réussies indique un défaut transversal après reconnaissance, et non sept défauts wire indépendants. + +## 2. Cause + +`MdCoreInstructionReplayInput.account_keys_json` est produit depuis l’en-tête du message Solana. Ses champs `signer` et `writable` représentent donc les privilèges globaux de chaque clé pour l’ensemble de la transaction. + +Le décodeur comparait ces champs par égalité avec les privilèges minimaux documentés pour chaque position de l’instruction. Cette égalité est invalide : une autorité peut être readonly dans le contrat de l’instruction tout en étant writable au niveau du message parce qu’elle est aussi fee payer ou intervient comme compte writable dans une autre instruction. + +Le programme on-chain vérifie les privilèges nécessaires, notamment `authority.is_signer()`, mais ne refuse pas un sur-ensemble de privilèges fourni par le message. + +## 3. Correction + +La validation applique désormais les règles suivantes : + +- un compte déclaré signer doit être signer dans le message ; +- un compte déclaré writable doit être writable dans le message ; +- un compte déclaré readonly peut être globalement writable ; +- un compte déclaré non-signer peut être globalement signer ; +- les positions, rôles, placeholders optionnels et contrats de source restent vérifiés séparément ; +- l’absence d’un privilège requis reste un échec fermé. + +Une régression couvre les neuf instructions stables avec une autorité globalement signer et writable. + +## 4. Validation à rejouer + +Après application du correctif, relancer les 132 entrées en échec, de préférence avec le replay forcé limité au programme `ProgM6…`. Le résultat attendu est : + +- `decoded = 132` ; +- `failed = 0` ; +- une matérialisation pour chaque instruction committée ; +- `trim` et `extend` restent non observés dans cet échantillon et devront être couverts par les scénarios Devnet. diff --git a/docs/plans/V0_4_8_SOLANA_PROGRAM_METADATA_AND_TOKEN_2022_COMPLETENESS_PLAN.md b/docs/plans/V0_4_8_SOLANA_PROGRAM_METADATA_AND_TOKEN_2022_COMPLETENESS_PLAN.md index 1420e86..911e3c6 100644 --- a/docs/plans/V0_4_8_SOLANA_PROGRAM_METADATA_AND_TOKEN_2022_COMPLETENESS_PLAN.md +++ b/docs/plans/V0_4_8_SOLANA_PROGRAM_METADATA_AND_TOKEN_2022_COMPLETENESS_PLAN.md @@ -1,11 +1,11 @@ - + # Plan `0.4.8` — Solana Program Metadata et complétude Token-2022 ## 1. Statut et rôle -Ce document est le livrable principal de `0.4.8-pre.001`, réconcilié après l’audit contractuel de `0.4.8-pre.002`, les modèles de `pre.003`, les décodeurs de comptes de `pre.004`, le décodeur d’instructions de `pre.005`, la matérialisation de `pre.006` et l’exécuteur de `pre.007`. Il constitue le plan vivant de la version et reste modifiable lorsque l’audit du code, des interfaces officielles, de l’IDL ou des validations Devnet impose un ajustement. +Ce document est le livrable principal de `0.4.8-pre.001`, réconcilié après l’audit contractuel de `0.4.8-pre.002`, les modèles de `pre.003`, les décodeurs de comptes de `pre.004`, le décodeur d’instructions de `pre.005`, la matérialisation de `pre.006`, l’exécuteur de `pre.007` et l’orchestration généraliste de `pre.008`. Il constitue le plan vivant de la version et reste modifiable lorsque l’audit du code, des interfaces officielles, de l’IDL ou des validations Devnet impose un ajustement. Il doit être maintenu pendant chaque prerelease, puis archivé pendant la dernière prerelease après transfert des décisions durables vers le ROADMAP, les matrices, les guides, les rapports, les TODO et les changelogs concernés. @@ -274,23 +274,37 @@ Résultat durable : les neuf instructions stables possèdent une opération typ - aucune opération stable classée deprecated ou replaced faute de preuve officielle ; - financement de rent volontairement laissé à la fixture et à l’orchestration RPC : le programme exige que les comptes à allouer ou étendre soient préfinancés. -Travaux parallèles intégrés : les fonctions de matérialisation Metaplex existaient déjà dans `materializer/metadata/core.rs`; il ne s’agissait donc pas d’un oubli fonctionnel de `0.4.6/0.4.7`. Elles sont déplacées dans un sous-module dédié sans changement de clé métier. Token-2022 reçoit également son composant state-only dédié. L’audit est ensuite étendu à l’ensemble de `src/materializer` : 25 identités sont harmonisées, les façades deviennent pures, les implémentations utilisent `materializer.rs`, les constantes sont centralisées et les coquilles réservées ne produisent plus de faux événements legacy. Les constats sont consignés dans `docs/audits/V0_4_8_PRE_007_METAPLEX_MATERIALIZER_STRUCTURE_AUDIT.md` et `docs/audits/V0_4_8_PRE_007_MATERIALIZER_CONVENTION_AUDIT.md`. Un audit transversal de toutes les surfaces déjà livrées est planifié en `0.5.x` pour vérifier les règles de décodage exhaustif, propriété de matérialisation, exécution des opérations dangereuses via `ExSafetyChecker`, dépréciation officielle et versions remplacées decode-only. +Travaux parallèles intégrés : les fonctions de matérialisation Metaplex existaient déjà dans `materializer/metadata/core.rs`; il ne s’agissait donc pas d’un oubli fonctionnel de `0.4.6/0.4.7`. Elles sont déplacées dans un sous-module dédié sans changement de clé métier. Token-2022 reçoit également son composant dédié de snapshots, puis `pre.008` ajoute son matérialiseur instructionnel spécialisé ainsi que celui de Metaplex. L’audit est ensuite étendu à l’ensemble de `src/materializer` : 25 identités sont harmonisées, les façades deviennent pures, les implémentations utilisent `materializer.rs`, les constantes sont centralisées et les coquilles réservées ne produisent plus de faux événements legacy. Les constats sont consignés dans `docs/audits/V0_4_8_PRE_007_METAPLEX_MATERIALIZER_STRUCTURE_AUDIT.md` et `docs/audits/V0_4_8_PRE_007_MATERIALIZER_CONVENTION_AUDIT.md`. Un audit transversal de toutes les surfaces déjà livrées est planifié en `0.5.x` pour vérifier les règles de décodage exhaustif, propriété de matérialisation, exécution des opérations dangereuses via `ExSafetyChecker`, dépréciation officielle et versions remplacées decode-only. -### `0.4.8-pre.008` — pipeline stateful Solana Program Metadata +### `0.4.8-pre.008` — pipeline stateful Solana Program Metadata — code terminé, replay historique à revalider -- simulation, soumission, confirmation, lectures avant/après, postconditions, replay et matérialisation. +- lectures RPC bornées des comptes absents, non initialisés, `Buffer` et `Metadata` ; +- décodage et matérialisation autoritative des états via `kb-lib` ; +- préflight des neuf opérations, vérification des sources Buffer, autorités, canonicité et preuves de rent ; +- passage par `ExSafetyChecker`, readiness simulation-first, résolution des signers et autorisation de soumission ; +- postconditions avant/après des neuf opérations, sans succès inventé lorsqu’une lecture manque ; +- matrice pipeline machine-readable ; +- enregistrement du décodeur et des trois matérialiseurs metadata spécialisés dans le replay générique desktop pour le futur test `demo_backfill` ; +- nomenclature interne `metadata_*`, `spl_*` et `solana_*` alignée sur les domaines de `kb-lib` ; +- aucune modification de `kb-pipeline-demo-scenarios` ni de `demo_execution_metadata`. +- audit consigné dans `docs/audits/V0_4_8_PRE_008_METADATA_SOLANA_PROGRAM_PIPELINE_AUDIT.md`. +- première campagne historique : 200 transactions importées, 582 instructions rejouées et 132 instructions `ProgM6…` reconnues ; toutes ont été rejetées à tort parce que le décodeur comparait les privilèges globaux du message par égalité avec les minima de l’instruction ; +- `pre.008-fix-002` remplace cette égalité par une validation de minima et ajoute une régression sur les neuf instructions ; la campagne des 132 entrées doit être rejouée avant d’ouvrir `pre.009`. -### `0.4.8-pre.009` — campagne Devnet Solana Program Metadata +### `0.4.8-pre.009` — campagne Devnet et desktop Solana Program Metadata -- fixture Rust native ; -- scénarios réutilisables ; +- fixture Rust native et comptes préfinancés ; +- scénarios réutilisables dans `kb-pipeline-demo-scenarios` ; +- appels RPC de simulation, soumission, confirmation, lectures avant/après et replay ; +- adaptation du sous-panneau `ProgM6…` de `demo_execution_metadata` pour consommer les runners réutilisables ; +- validation historique préalable via `demo_backfill`, Core extraction et decode replay sur un échantillon de signatures ; - preuves RPC et rapport intermédiaire. ### `0.4.8-pre.010` — complétude Token-2022 Token Metadata -- implémentation des écarts confirmés de `Initialize`, `UpdateField`, `RemoveKey`, `UpdateAuthority` et `Emit` ; -- conservation du matérialiseur d’état dédié `metadata/token_2022/` pour les extensions `TokenMetadata`, `TokenGroup` et `TokenGroupMember` ; -- ajout d’une matérialisation explicite des cinq observations d’instructions `spl-token-metadata-interface`, actuellement absente ; +- fermeture des écarts d’intents/builders/exécution confirmés pour `UpdateAuthority` et `Emit`, après réaudit des cinq instructions `Initialize`, `UpdateField`, `RemoveKey`, `UpdateAuthority` et `Emit` ; +- conservation des snapshots dédiés `metadata/token_2022/` pour `TokenMetadata`, `TokenGroup` et `TokenGroupMember` ; +- validation de la matérialisation instructionnelle spécialisée déjà ajoutée en `pre.008` pour les interfaces Token Metadata et Token Group ; - maintien de `InitializeMetadataPointer` et `UpdateMetadataPointer` dans le matérialiseur d’administration qui en possède déjà les faits ; - tests synthétiques et stateful. @@ -299,11 +313,12 @@ Travaux parallèles intégrés : les fonctions de matérialisation Metaplex exis - fixture mint ; - simulations, soumissions sûres, confirmations, postconditions, replay et matérialisation. -### `0.4.8-pre.012` — desktop metadata +### `0.4.8-pre.012` — finalisation desktop metadata -- accordéons ou sous-panneaux séparés pour `ProgM6…`, Token-2022 et Metaplex ; -- réutilisation du JsonViewer commun pour les résultats intégralement JSON ; -- préparation, simulation, soumission, lectures et preuves ; +- compléter le sous-panneau Token-2022 après les travaux de `pre.010` et `pre.011` ; +- conserver des accordéons ou sous-panneaux séparés pour `ProgM6…`, Token-2022 et Metaplex ; +- réutiliser le JsonViewer commun pour les résultats intégralement JSON ; +- réconcilier préparation, simulation, soumission, lectures et preuves des trois domaines ; - aucun bouton de fetch URI. ### `0.4.8-pre.013` — stockage, matrices et réconciliation transversale diff --git a/kb-app-demo-desktop/CHANGELOG.md b/kb-app-demo-desktop/CHANGELOG.md index 6bb9633..4ed06c7 100644 --- a/kb-app-demo-desktop/CHANGELOG.md +++ b/kb-app-demo-desktop/CHANGELOG.md @@ -1,8 +1,15 @@ - + # CHANGELOG — kb-app-demo-desktop +## 0.4.8-pre.008 + +- enregistre `metadata.solana_program_metadata` dans le replay générique afin que les signatures hydratées par `demo_backfill` puissent être décodées ; +- enregistre les trois matérialiseurs spécialisés `MtMetadataMetaplexTokenMetadataMaterializer`, `MtMetadataSolanaProgramMaterializer` et `MtMetadataToken2022Materializer` dans le replay générique ; +- vérifie par test qu’une observation représentative de chaque domaine metadata possède exactement un matérialiseur runtime ; +- ne modifie ni `demo_execution_metadata`, ni les scénarios Devnet, réservés à `0.4.8-pre.009`. + ## 0.4.8-pre.001 - aligne les versions Tauri et frontend sur `0.4.8` pour ouvrir la nouvelle version fonctionnelle ; diff --git a/kb-app-demo-desktop/README.md b/kb-app-demo-desktop/README.md index e80adbb..72c7143 100644 --- a/kb-app-demo-desktop/README.md +++ b/kb-app-demo-desktop/README.md @@ -1,5 +1,5 @@ - + # kb-app-demo-desktop @@ -10,7 +10,7 @@ - configuration et diagnostics de logging ; - transport HTTP JSON-RPC et WebSocket ; - diagnostics PostgreSQL ; -- backfill, extraction Core et replay ; +- backfill, extraction Core et replay, avec enregistrement générique de Solana Program Metadata ; - démonstrations Solana Core, SPL Memo, SPL Token, ATA et Token-2022 ; - panneau Metadata avec scénarios synthétiques, préparation du profil et de la base, intents typés, simulation RPC Devnet réelle et soumission explicitement confirmée ; - progression, annulation et restauration de l’état des fenêtres. diff --git a/kb-app-demo-desktop/USAGE.md b/kb-app-demo-desktop/USAGE.md index 65c9f4e..fd80bfe 100644 --- a/kb-app-demo-desktop/USAGE.md +++ b/kb-app-demo-desktop/USAGE.md @@ -1,5 +1,5 @@ - + # Utilisation de kb-app-demo-desktop @@ -151,6 +151,17 @@ console.log(options, summary); Une campagne longue peut être annulée avec `demo_core_extraction_cancel`. +### Validation historique Solana Program Metadata + +Avant les scénarios Devnet, utiliser les fenêtres généralistes dans cet ordre : + +1. `demo_backfill` avec un échantillon borné de signatures confirmées contenant `ProgM6JCCvbYkfKqJYHePx4xxSUSqJp7rh8Lyv7nk7S` ; +2. extraction Core sur les mêmes signatures ; +3. decode replay avec le Program ID `ProgM6…`, le décodeur `metadata.solana_program_metadata` et la matérialisation activée ; +4. lecture des événements du processor `materializer.metadata.solana_program_metadata`. + +Ce parcours ne dépend pas de `kb-pipeline-demo-scenarios`. La fenêtre `demo_execution_metadata` et les scénarios Devnet `ProgM6…` seront ajoutés séparément dans `0.4.8-pre.009`. + ### Decode replay ```typescript diff --git a/kb-app-demo-desktop/src/demo_decode_replay.rs b/kb-app-demo-desktop/src/demo_decode_replay.rs index 5496fba..29a0728 100644 --- a/kb-app-demo-desktop/src/demo_decode_replay.rs +++ b/kb-app-demo-desktop/src/demo_decode_replay.rs @@ -1,5 +1,5 @@ // file: kb-app-demo-desktop/src/demo_decode_replay.rs -// version: 37 +// version: 39 //! Tauri commands and UI payloads for contextual instruction decode replay. @@ -662,10 +662,13 @@ fn available_materializers() -> std::vec::Vec std::vec::Vec kb_lib::DcApiDecodedObservation { + return kb_lib::DcApiDecodedObservation { + event_key: format!("{surface}:{entry}:0"), + event: kb_lib::MdDecodedProtocolEvent { + signature: kb_lib::MdSignature("signature".to_string()), + slot: kb_lib::MdSlot(1), + instruction_path: kb_lib::MdInstructionPath("0".to_string()), + program_id: kb_lib::MdProgramId(program_id.to_string()), + protocol_code: kb_lib::MdProtocolCode(surface.to_string()), + surface_code: kb_lib::MdSurfaceCode(surface.to_string()), + event_code: kb_lib::MdEventCode(format!("{surface}.{entry}")), + event_name: kb_lib::MdEventName(entry.to_string()), + event_family: kb_lib::MdEventFamily::Metadata, + source_kind: kb_lib::MdEventSourceKind::Instruction, + confidence: kb_lib::MdDecoderConfidence::ManualExact, + }, + payload_json: serde_json::json!({ + "programId": program_id, + "instruction": entry, + "parameters": {}, + "accounts": [] + }), + transaction_failed: false, + transaction_error: std::option::Option::None, + observation_committed: true, + proof: kb_lib::DcApiDecoderProof { + kind: kb_lib::DcApiDecoderProofKind::Manual, + confidence: kb_lib::MdDecoderConfidence::ManualExact, + evidence: std::vec!["fixture".to_string()], + }, + }; + } + #[test] - fn native_memo_token_2022_elgamal_classic_token_and_ata_decoders_are_registered() { + fn current_core_spl_and_metadata_decoders_are_registered() { let decoders = super::available_decoders(); - assert_eq!(decoders.len(), 7); + assert_eq!(decoders.len(), 8); let mut names = decoders .iter() .map(|decoder| return decoder.identity().name) @@ -1031,6 +1072,7 @@ mod tests { names.sort(); let mut expected_names = std::vec![ "metadata.metaplex_token_metadata".to_string(), + "metadata.solana_program_metadata".to_string(), "solana.core".to_string(), "spl.associated_token_account".to_string(), "spl.elgamal_registry".to_string(), @@ -1045,7 +1087,7 @@ mod tests { #[test] fn runtime_materializer_registry_is_complete_for_current_instructional_surfaces() { let materializers = super::available_materializers(); - assert_eq!(materializers.len(), 8); + assert_eq!(materializers.len(), 11); let names = materializers .iter() .map(|materializer| return materializer.identity().name) @@ -1057,6 +1099,9 @@ mod tests { "materializer.compliance.audit".to_string(), "materializer.fees".to_string(), "materializer.lifecycle".to_string(), + "materializer.metadata.metaplex_token_metadata".to_string(), + "materializer.metadata.solana_program_metadata".to_string(), + "materializer.metadata.token_2022".to_string(), "materializer.staking".to_string(), "materializer.token.accounts".to_string(), "materializer.risk".to_string(), @@ -1065,6 +1110,38 @@ mod tests { ); } + #[test] + fn metadata_materializer_ownership_is_exact_in_the_runtime_registry() { + for (program_id, surface, entry, expected) in [ + ( + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + "metadata.metaplex_token_metadata", + "create_metadata_account_v3", + "materializer.metadata.metaplex_token_metadata", + ), + ( + kb_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID, + "metadata.solana_program_metadata", + "initialize", + "materializer.metadata.solana_program_metadata", + ), + ( + kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID, + "spl.token_2022", + "initialize_token_metadata", + "materializer.metadata.token_2022", + ), + ] { + let observation = metadata_observation(program_id, surface, entry); + let owners = super::available_materializers() + .iter() + .filter(|materializer| return materializer.accepts_observation(&observation)) + .map(|materializer| return materializer.identity().name) + .collect::>(); + assert_eq!(owners, std::vec![expected.to_string()]); + } + } + #[test] fn ata_materializer_ownership_is_exact_in_the_runtime_registry() { for (entry, expected) in [ diff --git a/kb-lib/CHANGELOG.md b/kb-lib/CHANGELOG.md index b16fb71..23065d9 100644 --- a/kb-lib/CHANGELOG.md +++ b/kb-lib/CHANGELOG.md @@ -1,8 +1,19 @@ - + # CHANGELOG — kb-lib +## 0.4.8-pre.008 — matérialiseurs metadata spécialisés + +- corrige le décodeur Solana Program Metadata pour interpréter les flags `signer` et `writable` du replay comme des privilèges globaux de message : les privilèges requis sont désormais des minima, et les sur-ensembles légitimes dus au fee payer ou à d’autres instructions ne sont plus rejetés ; +- ajoute une régression couvrant les neuf instructions avec une autorité signer et writable au niveau du message, tout en conservant le refus des privilèges requis manquants ; +- corrige la déclaration Rust de `MT_METADATA_METAPLEX_TOKEN_METADATA_ACCEPTED_FAMILIES` livrée dans `pre.008-fix-001`. +- remplace le type ambigu `MtSolanaProgramMetadataMaterializer` par `MtMetadataSolanaProgramMaterializer` et retire le wrapper générique `MtMetadataMaterializer` ; +- ajoute `MtMetadataMetaplexTokenMetadataMaterializer` et `MtMetadataToken2022Materializer`, chacun avec ses deux contrats, son identité, son tracing target et un ownership instructionnel exact ; +- conserve séparément les APIs de snapshots Metaplex, Solana Program Metadata et Token-2022, sans confondre fait d’instruction et état autoritatif ; +- classe les neuf instructions incorporées Token Metadata et Token Group sous la famille `Metadata`, afin qu’elles soient possédées par le matérialiseur metadata spécialisé plutôt que par un composant d’administration générique ; +- étend les tests de façade publique et les contrats d’inventaire aux trois matérialiseurs metadata spécialisés. + ## 0.4.8-pre.007 — exécuteur Solana Program Metadata et structure Metaplex - ajoute `ExMetadataSolanaProgramMetadataExecutor`, les intents typés, les helpers de PDA et les builders wire exacts des neuf instructions stables `0..8` ; @@ -24,9 +35,9 @@ - remplace les implémentations génériques `core.rs` par des `materializer.rs`, transforme les fichiers parents en façades pures et déplace toutes les familles, surfaces, tables et identités vers les `constants.rs` réexportés jusqu’à la racine de crate ; - rend les 14 matérialisateurs réservés réellement neutres sur les deux traits : famille vide constante, refus legacy, liste d’événements vide et résultat contextuel `Ignored` ; - aligne les routes de logging d’exemple sur `compliance.audit`, `transaction.annotations` et `token.accounts`, et ajoute des audits mécaniques empêchant la réintroduction de noms, constantes ou façades non conformes ; -- conserve `MtMetadataMaterializer` comme wrapper de compatibilité du composant canonique `MtSolanaProgramMetadataMaterializer`, sans second `processorName` ni second propriétaire de faits. +- prépare la spécialisation complète des matérialiseurs metadata, finalisée en `0.4.8-pre.008` sans wrapper générique ambigu. - corrige les warnings de contrats opérationnels en ajoutant des événements `tracing` réels aux matérialisations `fees`, Metaplex Token Metadata et metadata Token-2022, avec `component_name`, `processor_name`, statut, identité de compte ou d’instruction et nombre de sorties ; -- consomme désormais l’identité runtime `MT_METADATA_TOKEN_2022_COMPONENT_NAME` dans l’implémentation state-only au lieu de la limiter aux réexports, et étend les audits pour refuser tout tracing target opérationnel déclaré mais inutilisé ainsi que toute identité de composant absente de son implémentation. +- consomme désormais l’identité runtime `MT_METADATA_TOKEN_2022_COMPONENT_NAME` dans les opérations de snapshot, puis étend les audits pour refuser tout tracing target opérationnel déclaré mais inutilisé ainsi que toute identité de composant absente de son implémentation. ## 0.4.8-pre.006 — matérialisation Solana Program Metadata diff --git a/kb-lib/README.md b/kb-lib/README.md index 4277596..2d7d024 100644 --- a/kb-lib/README.md +++ b/kb-lib/README.md @@ -1,5 +1,5 @@ - + # kb-lib @@ -48,7 +48,7 @@ L’arbre `src/materializer` suit un contrat homogène : - une identité runtime `kb-lib.materializer.*` distincte du `processorName` persisté `materializer.*` ; - des coquilles réservées réellement inactives sur les deux contrats legacy et contextuel. -L’inventaire audité comprend 25 composants nommés : neuf matérialisateurs actifs, quatorze coquilles réservées et deux composants metadata state-only. Le détail est consigné dans [l’audit de convention des matérialisateurs](../docs/audits/V0_4_8_PRE_007_MATERIALIZER_CONVENTION_AUDIT.md). +L’inventaire audité comprend 25 composants nommés : onze matérialisateurs actifs et quatorze coquilles réservées. Les composants metadata Metaplex Token Metadata, Solana Program Metadata et Token-2022 possèdent chacun un type spécialisé, une identité propre et leurs APIs de snapshots lorsque leur état on-chain l’exige. Le détail est consigné dans [l’audit de convention des matérialisateurs](../docs/audits/V0_4_8_PRE_007_MATERIALIZER_CONVENTION_AUDIT.md). ## Responsabilités diff --git a/kb-lib/USAGE.md b/kb-lib/USAGE.md index 7f9f6f1..95d91ce 100644 --- a/kb-lib/USAGE.md +++ b/kb-lib/USAGE.md @@ -1,5 +1,5 @@ - + # Utilisation de kb-lib @@ -153,10 +153,16 @@ println!("status={:?}", result.status); Le tag `5` est toujours publié comme `trim`. Son ancien nom pré-stable `withdraw_excess_lamports` est conservé uniquement comme provenance, car aucune release stable ne l’expose comme instruction distincte. Les URL et références externes restent des données on-chain ; le décodeur ne réalise aucun fetch. -Le matérialiseur `MtMetadataMaterializer` accepte les neuf observations d’instructions de la surface exacte `metadata.solana_program_metadata`. Il refuse les transactions échouées et produit un fait de mutation par instruction. Ces faits ne remplacent pas un snapshot de compte : l’état final doit être matérialisé depuis la lecture post-exécution. +Les trois domaines metadata disposent de matérialiseurs instructionnels spécialisés : + +- `MtMetadataMetaplexTokenMetadataMaterializer` pour les observations Metaplex de famille metadata ; +- `MtMetadataSolanaProgramMaterializer` pour les neuf instructions stables de `ProgM6…` ; +- `MtMetadataToken2022Materializer` pour les instructions incorporées Token Metadata et Token Group de Token-2022. + +Chaque type possède son propre `componentName`, son propre `processorName`, ses familles acceptées et un ownership exact. Les transactions échouées sont refusées. Les faits d’instructions ne remplacent jamais les snapshots autoritatifs lus après exécution. ```rust -let materializer = kb_lib::MtMetadataMaterializer; +let materializer = kb_lib::MtMetadataSolanaProgramMaterializer; if kb_lib::MtApiEventMaterializer::accepts_observation(&materializer, &observation) { let result = kb_lib::MtApiEventMaterializer::materialize(&materializer, &observation); println!("materialization status={:?}", result.status); diff --git a/kb-lib/src/decoder/metadata/solana_program_metadata/instruction.rs b/kb-lib/src/decoder/metadata/solana_program_metadata/instruction.rs index 6fcd6bb..5911cfc 100644 --- a/kb-lib/src/decoder/metadata/solana_program_metadata/instruction.rs +++ b/kb-lib/src/decoder/metadata/solana_program_metadata/instruction.rs @@ -1,5 +1,5 @@ // file: kb-lib/src/decoder/metadata/solana_program_metadata/instruction.rs -// version: 4 +// version: 5 //! Exact contextual instruction decoding for Solana Program Metadata. @@ -627,9 +627,15 @@ fn validate_accounts( )); }, }; - if account.signer != contract.signer || account.writable != contract.writable { + if contract.signer && !account.signer { return std::result::Result::Err(format!( - "{} account {position} ({}) has invalid signer/writable flags", + "{} account {position} ({}) is missing the required signer privilege", + parsed.code, contract.role + )); + } + if contract.writable && !account.writable { + return std::result::Result::Err(format!( + "{} account {position} ({}) is missing the required writable privilege", parsed.code, contract.role )); } @@ -1160,6 +1166,115 @@ mod tests { } } + #[test] + fn transaction_wide_privilege_supersets_are_accepted() { + let cases = [ + ( + vec![0, 4, 0, 0, 0, 1, 2], + vec![ + ("buffer", false, true), + ("authority", true, true), + (placeholder(), false, false), + ], + "write", + ), + ( + [vec![1], vec![0; 16], vec![1, 0, 1, 0], vec![1]].concat(), + vec![ + ("metadata", false, true), + ("authority", true, true), + ("program", false, false), + (placeholder(), false, false), + (placeholder(), false, false), + ], + "initialize", + ), + ( + vec![2, 0], + vec![ + ("account", false, true), + ("authority", true, true), + (placeholder(), false, false), + (placeholder(), false, false), + ], + "set_authority", + ), + ( + vec![3, 1, 0, 1], + vec![ + ("metadata", false, true), + ("authority", true, true), + (placeholder(), false, true), + (placeholder(), false, false), + (placeholder(), false, false), + ], + "set_data", + ), + ( + vec![4], + vec![ + ("metadata", false, true), + ("authority", true, true), + (placeholder(), false, false), + (placeholder(), false, false), + ], + "set_immutable", + ), + ( + vec![5], + vec![ + ("account", false, true), + ("authority", true, true), + (placeholder(), false, false), + (placeholder(), false, false), + ("destination", false, true), + ("SysvarRent111111111111111111111111111111111", false, false), + ], + "trim", + ), + ( + vec![6], + vec![ + ("account", false, true), + ("authority", true, true), + (placeholder(), false, false), + (placeholder(), false, false), + ("destination", false, true), + ], + "close", + ), + ( + [vec![7], vec![8; 16]].concat(), + vec![ + ("buffer", false, true), + ("authority", true, true), + ("program", false, false), + (placeholder(), false, false), + (placeholder(), false, false), + ], + "allocate", + ), + ( + vec![8, 32, 0], + vec![ + ("account", false, true), + ("authority", true, true), + (placeholder(), false, false), + (placeholder(), false, false), + ], + "extend", + ), + ]; + for (payload, accounts, expected) in cases { + let result = decode(payload.as_slice(), accounts.as_slice()); + assert_eq!(result.status, crate::DcApiDecoderOutcomeStatus::Decoded, "{expected}"); + assert_eq!( + result.recognized_entry_code.as_deref(), + std::option::Option::Some(expected) + ); + } + } + #[test] fn source_selection_and_account_flags_fail_closed() { let invalid_write = decode( @@ -1199,6 +1314,16 @@ mod tests { ], ); assert_eq!(invalid_flags.status, crate::DcApiDecoderOutcomeStatus::Failed); + let missing_signer = decode( + &[8, 1, 0], + &[ + ("account", false, true), + ("authority", false, true), + (placeholder(), false, false), + (placeholder(), false, false), + ], + ); + assert_eq!(missing_signer.status, crate::DcApiDecoderOutcomeStatus::Failed); } #[test] diff --git a/kb-lib/src/decoder/spl/token_2022/wire.rs b/kb-lib/src/decoder/spl/token_2022/wire.rs index 06b25e4..38d1e35 100644 --- a/kb-lib/src/decoder/spl/token_2022/wire.rs +++ b/kb-lib/src/decoder/spl/token_2022/wire.rs @@ -1,5 +1,5 @@ // file: kb-lib/src/decoder/spl/token_2022/wire.rs -// version: 2 +// version: 3 //! Bounded Token-2022 wire, account and authority decoding. @@ -2532,10 +2532,7 @@ fn event_family(code: &str) -> crate::MdEventFamily { | "initialize_multisig2" | "initialize_permanent_delegate" | "initialize_interest_bearing_mint" - | "update_interest_bearing_rate" - | "update_token_metadata_authority" - | "update_token_group_authority" - | "update_token_group_max_size" => crate::MdEventFamily::Admin, + | "update_interest_bearing_rate" => crate::MdEventFamily::Admin, "initialize_mint" | "initialize_mint2" | "mint_to" @@ -2564,8 +2561,11 @@ fn event_family(code: &str) -> crate::MdEventFamily { "initialize_token_metadata" | "update_token_metadata_field" | "remove_token_metadata_key" + | "update_token_metadata_authority" | "emit_token_metadata" | "initialize_token_group" + | "update_token_group_max_size" + | "update_token_group_authority" | "initialize_token_group_member" => crate::MdEventFamily::Metadata, "batch" | "get_account_data_size" | "amount_to_ui_amount" | "ui_amount_to_amount" => { crate::MdEventFamily::Audit @@ -2608,3 +2608,23 @@ fn failed( }], }; } + +#[cfg(test)] +mod metadata_family_tests { + #[test] + fn every_embedded_metadata_and_group_instruction_uses_metadata_family() { + for operation in [ + "initialize_token_metadata", + "update_token_metadata_field", + "remove_token_metadata_key", + "update_token_metadata_authority", + "emit_token_metadata", + "initialize_token_group", + "update_token_group_max_size", + "update_token_group_authority", + "initialize_token_group_member", + ] { + assert_eq!(super::event_family(operation), crate::MdEventFamily::Metadata); + } + } +} diff --git a/kb-lib/src/lib.rs b/kb-lib/src/lib.rs index 62fc277..5b1a4f4 100644 --- a/kb-lib/src/lib.rs +++ b/kb-lib/src/lib.rs @@ -1,5 +1,5 @@ // file: kb-lib/src/lib.rs -// version: 36 +// version: 37 //! Consolidated decoder, executor, materializer and shared model library. #![warn(missing_docs)] @@ -1439,8 +1439,12 @@ pub use self::materializer::MtLifecycleMaterializer; pub use self::materializer::MtLiquidityMaterializer; /// Exposes the legacy common business materializer trait. pub use self::materializer::MtMaterializer; -/// Compatibility wrapper for the Solana Program Metadata materializer. -pub use self::materializer::MtMetadataMaterializer; +/// Metaplex Token Metadata instruction materializer. +pub use self::materializer::MtMetadataMetaplexTokenMetadataMaterializer; +/// Solana Program Metadata instruction materializer. +pub use self::materializer::MtMetadataSolanaProgramMaterializer; +/// SPL Token-2022 embedded metadata and group instruction materializer. +pub use self::materializer::MtMetadataToken2022Materializer; /// Reserved NFT materializer. pub use self::materializer::MtNftMaterializer; /// Reserved oracle materializer. @@ -1457,8 +1461,6 @@ pub use self::materializer::MtRewardsMaterializer; pub use self::materializer::MtRiskMaterializer; /// Reserved routing materializer. pub use self::materializer::MtRoutingMaterializer; -/// Stable Solana Program Metadata instruction materializer. -pub use self::materializer::MtSolanaProgramMetadataMaterializer; /// Stable native stake and vote materializer. pub use self::materializer::MtStakingMaterializer; /// Stable Token, ATA and Token-2022 account materializer. @@ -2250,22 +2252,28 @@ pub(crate) use self::materializer::MT_LIQUIDITY_ACCEPTED_FAMILIES; pub(crate) use self::materializer::MT_LIQUIDITY_COMPONENT_NAME; /// Crate-root access to `MT_LIQUIDITY_PROCESSOR_NAME`. pub(crate) use self::materializer::MT_LIQUIDITY_PROCESSOR_NAME; +/// Crate-root access to `MT_METADATA_METAPLEX_TOKEN_METADATA_ACCEPTED_FAMILIES`. +pub(crate) use self::materializer::MT_METADATA_METAPLEX_TOKEN_METADATA_ACCEPTED_FAMILIES; /// Crate-root access to `MT_METADATA_METAPLEX_TOKEN_METADATA_COMPONENT_NAME`. pub(crate) use self::materializer::MT_METADATA_METAPLEX_TOKEN_METADATA_COMPONENT_NAME; /// Crate-root access to `MT_METADATA_METAPLEX_TOKEN_METADATA_PROCESSOR_NAME`. pub(crate) use self::materializer::MT_METADATA_METAPLEX_TOKEN_METADATA_PROCESSOR_NAME; /// Crate-root access to `MT_METADATA_METAPLEX_TOKEN_METADATA_PROJECTION_VERSION`. pub(crate) use self::materializer::MT_METADATA_METAPLEX_TOKEN_METADATA_PROJECTION_VERSION; -/// Crate-root access to `MT_METADATA_SOLANA_PROGRAM_METADATA_ACCEPTED_FAMILIES`. -pub(crate) use self::materializer::MT_METADATA_SOLANA_PROGRAM_METADATA_ACCEPTED_FAMILIES; -/// Crate-root access to `MT_METADATA_SOLANA_PROGRAM_METADATA_COMPONENT_NAME`. -pub(crate) use self::materializer::MT_METADATA_SOLANA_PROGRAM_METADATA_COMPONENT_NAME; -/// Crate-root access to `MT_METADATA_SOLANA_PROGRAM_METADATA_PROCESSOR_NAME`. -pub(crate) use self::materializer::MT_METADATA_SOLANA_PROGRAM_METADATA_PROCESSOR_NAME; -/// Crate-root access to `MT_METADATA_SOLANA_PROGRAM_METADATA_PROJECTION_VERSION`. -pub(crate) use self::materializer::MT_METADATA_SOLANA_PROGRAM_METADATA_PROJECTION_VERSION; +/// Crate-root access to `MT_METADATA_SOLANA_PROGRAM_ACCEPTED_FAMILIES`. +pub(crate) use self::materializer::MT_METADATA_SOLANA_PROGRAM_ACCEPTED_FAMILIES; +/// Crate-root access to `MT_METADATA_SOLANA_PROGRAM_COMPONENT_NAME`. +pub(crate) use self::materializer::MT_METADATA_SOLANA_PROGRAM_COMPONENT_NAME; +/// Crate-root access to `MT_METADATA_SOLANA_PROGRAM_PROCESSOR_NAME`. +pub(crate) use self::materializer::MT_METADATA_SOLANA_PROGRAM_PROCESSOR_NAME; +/// Crate-root access to `MT_METADATA_SOLANA_PROGRAM_PROJECTION_VERSION`. +pub(crate) use self::materializer::MT_METADATA_SOLANA_PROGRAM_PROJECTION_VERSION; +/// Crate-root access to `MT_METADATA_TOKEN_2022_ACCEPTED_FAMILIES`. +pub(crate) use self::materializer::MT_METADATA_TOKEN_2022_ACCEPTED_FAMILIES; /// Crate-root access to `MT_METADATA_TOKEN_2022_COMPONENT_NAME`. pub(crate) use self::materializer::MT_METADATA_TOKEN_2022_COMPONENT_NAME; +/// Crate-root access to `MT_METADATA_TOKEN_2022_INSTRUCTION_ENTRIES`. +pub(crate) use self::materializer::MT_METADATA_TOKEN_2022_INSTRUCTION_ENTRIES; /// Crate-root access to `MT_METADATA_TOKEN_2022_PROCESSOR_NAME`. pub(crate) use self::materializer::MT_METADATA_TOKEN_2022_PROCESSOR_NAME; /// Crate-root access to `MT_METADATA_TOKEN_2022_PROJECTION_VERSION`. @@ -2406,8 +2414,8 @@ pub(crate) use self::materializer::TRACING_TARGET_MATERIALIZER_FEES; pub(crate) use self::materializer::TRACING_TARGET_MATERIALIZER_LIFECYCLE; /// Crate-root access to `TRACING_TARGET_MATERIALIZER_METADATA_METAPLEX_TOKEN_METADATA`. pub(crate) use self::materializer::TRACING_TARGET_MATERIALIZER_METADATA_METAPLEX_TOKEN_METADATA; -/// Crate-root access to `TRACING_TARGET_MATERIALIZER_METADATA_SOLANA_PROGRAM_METADATA`. -pub(crate) use self::materializer::TRACING_TARGET_MATERIALIZER_METADATA_SOLANA_PROGRAM_METADATA; +/// Crate-root access to `TRACING_TARGET_MATERIALIZER_METADATA_SOLANA_PROGRAM`. +pub(crate) use self::materializer::TRACING_TARGET_MATERIALIZER_METADATA_SOLANA_PROGRAM; /// Crate-root access to `TRACING_TARGET_MATERIALIZER_METADATA_TOKEN_2022`. pub(crate) use self::materializer::TRACING_TARGET_MATERIALIZER_METADATA_TOKEN_2022; /// Crate-root access to `TRACING_TARGET_MATERIALIZER_RISK`. diff --git a/kb-lib/src/materializer.rs b/kb-lib/src/materializer.rs index 23fc8af..d4837cf 100644 --- a/kb-lib/src/materializer.rs +++ b/kb-lib/src/materializer.rs @@ -1,5 +1,5 @@ // file: kb-lib/src/materializer.rs -// version: 14 +// version: 15 //! Consolidated materializer modules. @@ -73,10 +73,12 @@ pub use self::lending::MtLendingMaterializer; pub use self::lifecycle::MtLifecycleMaterializer; /// Reserved liquidity materializer. pub use self::liquidity::MtLiquidityMaterializer; -/// Compatibility wrapper for the Solana Program Metadata materializer. -pub use self::metadata::MtMetadataMaterializer; -/// Stable Solana Program Metadata instruction materializer. -pub use self::metadata::MtSolanaProgramMetadataMaterializer; +/// Metaplex Token Metadata instruction materializer. +pub use self::metadata::MtMetadataMetaplexTokenMetadataMaterializer; +/// Solana Program Metadata instruction materializer. +pub use self::metadata::MtMetadataSolanaProgramMaterializer; +/// SPL Token-2022 embedded metadata and group instruction materializer. +pub use self::metadata::MtMetadataToken2022Materializer; /// Materializes one canonical non-Metadata Metaplex account snapshot. pub use self::metadata::materializer_metadata_materialize_metaplex_account_snapshot; /// Materializes one canonical Metaplex Metadata account snapshot. @@ -272,30 +274,36 @@ pub(crate) use self::liquidity::MT_LIQUIDITY_ACCEPTED_FAMILIES; pub(crate) use self::liquidity::MT_LIQUIDITY_COMPONENT_NAME; /// Crate-visible `MT_LIQUIDITY_PROCESSOR_NAME` component constant. pub(crate) use self::liquidity::MT_LIQUIDITY_PROCESSOR_NAME; +/// Crate-visible `MT_METADATA_METAPLEX_TOKEN_METADATA_ACCEPTED_FAMILIES` component constant. +pub(crate) use self::metadata::MT_METADATA_METAPLEX_TOKEN_METADATA_ACCEPTED_FAMILIES; /// Crate-visible `MT_METADATA_METAPLEX_TOKEN_METADATA_COMPONENT_NAME` component constant. pub(crate) use self::metadata::MT_METADATA_METAPLEX_TOKEN_METADATA_COMPONENT_NAME; /// Crate-visible `MT_METADATA_METAPLEX_TOKEN_METADATA_PROCESSOR_NAME` component constant. pub(crate) use self::metadata::MT_METADATA_METAPLEX_TOKEN_METADATA_PROCESSOR_NAME; /// Crate-visible `MT_METADATA_METAPLEX_TOKEN_METADATA_PROJECTION_VERSION` component constant. pub(crate) use self::metadata::MT_METADATA_METAPLEX_TOKEN_METADATA_PROJECTION_VERSION; -/// Crate-visible `MT_METADATA_SOLANA_PROGRAM_METADATA_ACCEPTED_FAMILIES` component constant. -pub(crate) use self::metadata::MT_METADATA_SOLANA_PROGRAM_METADATA_ACCEPTED_FAMILIES; -/// Crate-visible `MT_METADATA_SOLANA_PROGRAM_METADATA_COMPONENT_NAME` component constant. -pub(crate) use self::metadata::MT_METADATA_SOLANA_PROGRAM_METADATA_COMPONENT_NAME; -/// Crate-visible `MT_METADATA_SOLANA_PROGRAM_METADATA_PROCESSOR_NAME` component constant. -pub(crate) use self::metadata::MT_METADATA_SOLANA_PROGRAM_METADATA_PROCESSOR_NAME; -/// Crate-visible `MT_METADATA_SOLANA_PROGRAM_METADATA_PROJECTION_VERSION` component constant. -pub(crate) use self::metadata::MT_METADATA_SOLANA_PROGRAM_METADATA_PROJECTION_VERSION; +/// Crate-visible `MT_METADATA_SOLANA_PROGRAM_ACCEPTED_FAMILIES` component constant. +pub(crate) use self::metadata::MT_METADATA_SOLANA_PROGRAM_ACCEPTED_FAMILIES; +/// Crate-visible `MT_METADATA_SOLANA_PROGRAM_COMPONENT_NAME` component constant. +pub(crate) use self::metadata::MT_METADATA_SOLANA_PROGRAM_COMPONENT_NAME; +/// Crate-visible `MT_METADATA_SOLANA_PROGRAM_PROCESSOR_NAME` component constant. +pub(crate) use self::metadata::MT_METADATA_SOLANA_PROGRAM_PROCESSOR_NAME; +/// Crate-visible `MT_METADATA_SOLANA_PROGRAM_PROJECTION_VERSION` component constant. +pub(crate) use self::metadata::MT_METADATA_SOLANA_PROGRAM_PROJECTION_VERSION; +/// Crate-visible `MT_METADATA_TOKEN_2022_ACCEPTED_FAMILIES` component constant. +pub(crate) use self::metadata::MT_METADATA_TOKEN_2022_ACCEPTED_FAMILIES; /// Crate-visible `MT_METADATA_TOKEN_2022_COMPONENT_NAME` component constant. pub(crate) use self::metadata::MT_METADATA_TOKEN_2022_COMPONENT_NAME; +/// Crate-visible `MT_METADATA_TOKEN_2022_INSTRUCTION_ENTRIES` component constant. +pub(crate) use self::metadata::MT_METADATA_TOKEN_2022_INSTRUCTION_ENTRIES; /// Crate-visible `MT_METADATA_TOKEN_2022_PROCESSOR_NAME` component constant. pub(crate) use self::metadata::MT_METADATA_TOKEN_2022_PROCESSOR_NAME; /// Crate-visible `MT_METADATA_TOKEN_2022_PROJECTION_VERSION` component constant. pub(crate) use self::metadata::MT_METADATA_TOKEN_2022_PROJECTION_VERSION; /// Crate-visible `TRACING_TARGET_MATERIALIZER_METADATA_METAPLEX_TOKEN_METADATA` component constant. pub(crate) use self::metadata::TRACING_TARGET_MATERIALIZER_METADATA_METAPLEX_TOKEN_METADATA; -/// Crate-visible `TRACING_TARGET_MATERIALIZER_METADATA_SOLANA_PROGRAM_METADATA` component constant. -pub(crate) use self::metadata::TRACING_TARGET_MATERIALIZER_METADATA_SOLANA_PROGRAM_METADATA; +/// Crate-visible `TRACING_TARGET_MATERIALIZER_METADATA_SOLANA_PROGRAM` component constant. +pub(crate) use self::metadata::TRACING_TARGET_MATERIALIZER_METADATA_SOLANA_PROGRAM; /// Crate-visible `TRACING_TARGET_MATERIALIZER_METADATA_TOKEN_2022` component constant. pub(crate) use self::metadata::TRACING_TARGET_MATERIALIZER_METADATA_TOKEN_2022; /// Crate-visible `MT_NFT_ACCEPTED_FAMILIES` component constant. @@ -491,10 +499,22 @@ mod tests { crate::MT_LIQUIDITY_PROCESSOR_NAME, ), ( - &crate::MtSolanaProgramMetadataMaterializer, - &crate::MtSolanaProgramMetadataMaterializer, - crate::MT_METADATA_SOLANA_PROGRAM_METADATA_COMPONENT_NAME, - crate::MT_METADATA_SOLANA_PROGRAM_METADATA_PROCESSOR_NAME, + &crate::MtMetadataMetaplexTokenMetadataMaterializer, + &crate::MtMetadataMetaplexTokenMetadataMaterializer, + crate::MT_METADATA_METAPLEX_TOKEN_METADATA_COMPONENT_NAME, + crate::MT_METADATA_METAPLEX_TOKEN_METADATA_PROCESSOR_NAME, + ), + ( + &crate::MtMetadataSolanaProgramMaterializer, + &crate::MtMetadataSolanaProgramMaterializer, + crate::MT_METADATA_SOLANA_PROGRAM_COMPONENT_NAME, + crate::MT_METADATA_SOLANA_PROGRAM_PROCESSOR_NAME, + ), + ( + &crate::MtMetadataToken2022Materializer, + &crate::MtMetadataToken2022Materializer, + crate::MT_METADATA_TOKEN_2022_COMPONENT_NAME, + crate::MT_METADATA_TOKEN_2022_PROCESSOR_NAME, ), ( &crate::MtNftMaterializer, @@ -583,22 +603,6 @@ mod tests { ]; } - fn state_only_materializer_contracts() -> &'static [(&'static str, &'static str, &'static str)] - { - return &[ - ( - crate::MT_METADATA_METAPLEX_TOKEN_METADATA_COMPONENT_NAME, - crate::MT_METADATA_METAPLEX_TOKEN_METADATA_PROCESSOR_NAME, - crate::TRACING_TARGET_MATERIALIZER_METADATA_METAPLEX_TOKEN_METADATA, - ), - ( - crate::MT_METADATA_TOKEN_2022_COMPONENT_NAME, - crate::MT_METADATA_TOKEN_2022_PROCESSOR_NAME, - crate::TRACING_TARGET_MATERIALIZER_METADATA_TOKEN_2022, - ), - ]; - } - #[test] fn every_materializer_uses_the_component_and_processor_name_convention() { for (legacy, contextual, component_name, processor_name) in materializer_contracts() { @@ -615,18 +619,6 @@ mod tests { } } - #[test] - fn state_only_materializers_keep_runtime_processor_and_tracing_identity_aligned() { - for (component_name, processor_name, tracing_target) in state_only_materializer_contracts() - { - assert_eq!(*tracing_target, *component_name); - assert_eq!( - component_name.strip_prefix("kb-lib."), - std::option::Option::Some(*processor_name) - ); - } - } - #[test] fn reserved_materializers_are_empty_shells() { let materializers: &[&dyn crate::MtApiEventMaterializer] = &[ diff --git a/kb-lib/src/materializer/metadata.rs b/kb-lib/src/materializer/metadata.rs index 40d6282..dc9952c 100644 --- a/kb-lib/src/materializer/metadata.rs +++ b/kb-lib/src/materializer/metadata.rs @@ -1,5 +1,5 @@ // file: kb-lib/src/materializer/metadata.rs -// version: 8 +// version: 9 //! `metadata` materializer family. @@ -7,23 +7,27 @@ mod metaplex_token_metadata; mod solana_program_metadata; mod token_2022; +/// Metaplex Token Metadata instruction materializer. +pub use self::metaplex_token_metadata::MtMetadataMetaplexTokenMetadataMaterializer; /// Materializes one canonical non-Metadata Metaplex account snapshot. pub use self::metaplex_token_metadata::materializer_metadata_materialize_metaplex_account_snapshot; /// Materializes one canonical Metaplex Metadata account snapshot. pub use self::metaplex_token_metadata::materializer_metadata_materialize_metaplex_metadata_snapshot; -/// Compatibility materializer retaining the historical generic metadata type name. -pub use self::solana_program_metadata::MtMetadataMaterializer; -/// Stable Solana Program Metadata instruction materializer. -pub use self::solana_program_metadata::MtSolanaProgramMetadataMaterializer; +/// Solana Program Metadata instruction materializer. +pub use self::solana_program_metadata::MtMetadataSolanaProgramMaterializer; /// Materializes one decoded Solana Program Metadata account snapshot. pub use self::solana_program_metadata::materializer_metadata_materialize_solana_program_metadata_account_snapshot; /// Materializes one bounded Solana Program Metadata Buffer account snapshot. pub use self::solana_program_metadata::materializer_metadata_materialize_solana_program_metadata_buffer_snapshot; /// Materializes one bounded Solana Program Metadata Metadata account snapshot. pub use self::solana_program_metadata::materializer_metadata_materialize_solana_program_metadata_metadata_snapshot; +/// SPL Token-2022 embedded metadata and group instruction materializer. +pub use self::token_2022::MtMetadataToken2022Materializer; /// Materializes Token-2022 embedded metadata and group snapshots. pub use self::token_2022::materializer_metadata_materialize_token_2022_snapshot; +/// Event families accepted by the Metaplex Token Metadata instruction materializer. +pub(crate) use self::metaplex_token_metadata::MT_METADATA_METAPLEX_TOKEN_METADATA_ACCEPTED_FAMILIES; /// Stable runtime component name for Metaplex Token Metadata projections. pub(crate) use self::metaplex_token_metadata::MT_METADATA_METAPLEX_TOKEN_METADATA_COMPONENT_NAME; /// Stable processor name for Metaplex Token Metadata projections. @@ -33,17 +37,21 @@ pub(crate) use self::metaplex_token_metadata::MT_METADATA_METAPLEX_TOKEN_METADAT /// Canonical tracing target for Metaplex Token Metadata materialization. pub(crate) use self::metaplex_token_metadata::TRACING_TARGET_MATERIALIZER_METADATA_METAPLEX_TOKEN_METADATA; /// Event families accepted by the Solana Program Metadata instruction materializer. -pub(crate) use self::solana_program_metadata::MT_METADATA_SOLANA_PROGRAM_METADATA_ACCEPTED_FAMILIES; +pub(crate) use self::solana_program_metadata::MT_METADATA_SOLANA_PROGRAM_ACCEPTED_FAMILIES; /// Stable runtime component name for Solana Program Metadata projections. -pub(crate) use self::solana_program_metadata::MT_METADATA_SOLANA_PROGRAM_METADATA_COMPONENT_NAME; +pub(crate) use self::solana_program_metadata::MT_METADATA_SOLANA_PROGRAM_COMPONENT_NAME; /// Stable processor name for Solana Program Metadata projections. -pub(crate) use self::solana_program_metadata::MT_METADATA_SOLANA_PROGRAM_METADATA_PROCESSOR_NAME; +pub(crate) use self::solana_program_metadata::MT_METADATA_SOLANA_PROGRAM_PROCESSOR_NAME; /// Stable projection schema version for Solana Program Metadata outputs. -pub(crate) use self::solana_program_metadata::MT_METADATA_SOLANA_PROGRAM_METADATA_PROJECTION_VERSION; +pub(crate) use self::solana_program_metadata::MT_METADATA_SOLANA_PROGRAM_PROJECTION_VERSION; /// Canonical tracing target for Solana Program Metadata materialization. -pub(crate) use self::solana_program_metadata::TRACING_TARGET_MATERIALIZER_METADATA_SOLANA_PROGRAM_METADATA; +pub(crate) use self::solana_program_metadata::TRACING_TARGET_MATERIALIZER_METADATA_SOLANA_PROGRAM; +/// Event families accepted by the Token-2022 metadata instruction materializer. +pub(crate) use self::token_2022::MT_METADATA_TOKEN_2022_ACCEPTED_FAMILIES; /// Stable runtime component name for Token-2022 embedded metadata projections. pub(crate) use self::token_2022::MT_METADATA_TOKEN_2022_COMPONENT_NAME; +/// Exact embedded Token Metadata and Token Group instruction ownership. +pub(crate) use self::token_2022::MT_METADATA_TOKEN_2022_INSTRUCTION_ENTRIES; /// Stable processor name for Token-2022 embedded metadata projections. pub(crate) use self::token_2022::MT_METADATA_TOKEN_2022_PROCESSOR_NAME; /// Stable Token-2022 embedded metadata projection version. diff --git a/kb-lib/src/materializer/metadata/metaplex_token_metadata.rs b/kb-lib/src/materializer/metadata/metaplex_token_metadata.rs index ca65bc3..09471d5 100644 --- a/kb-lib/src/materializer/metadata/metaplex_token_metadata.rs +++ b/kb-lib/src/materializer/metadata/metaplex_token_metadata.rs @@ -1,17 +1,22 @@ // file: kb-lib/src/materializer/metadata/metaplex_token_metadata.rs -// version: 2 +// version: 3 //! Metaplex Token Metadata account-state materializer. mod account; mod constants; +mod materializer; mod state; /// Materializes one canonical non-Metadata Metaplex account snapshot. pub use self::account::materializer_metadata_materialize_metaplex_account_snapshot; +/// Metaplex Token Metadata instruction materializer. +pub use self::materializer::MtMetadataMetaplexTokenMetadataMaterializer; /// Materializes one canonical Metaplex Metadata account snapshot. pub use self::state::materializer_metadata_materialize_metaplex_metadata_snapshot; +/// Event families accepted by the Metaplex Token Metadata instruction materializer. +pub(crate) use self::constants::MT_METADATA_METAPLEX_TOKEN_METADATA_ACCEPTED_FAMILIES; /// Stable runtime component name for Metaplex Token Metadata projections. pub(crate) use self::constants::MT_METADATA_METAPLEX_TOKEN_METADATA_COMPONENT_NAME; /// Stable processor name for Metaplex Token Metadata projections. diff --git a/kb-lib/src/materializer/metadata/metaplex_token_metadata/constants.rs b/kb-lib/src/materializer/metadata/metaplex_token_metadata/constants.rs index 31c619a..c0f1a1f 100644 --- a/kb-lib/src/materializer/metadata/metaplex_token_metadata/constants.rs +++ b/kb-lib/src/materializer/metadata/metaplex_token_metadata/constants.rs @@ -1,5 +1,5 @@ // file: kb-lib/src/materializer/metadata/metaplex_token_metadata/constants.rs -// version: 4 +// version: 6 //! Local constants for the metadata metaplex_token_metadata materializer. @@ -9,6 +9,9 @@ pub(crate) const MT_METADATA_METAPLEX_TOKEN_METADATA_COMPONENT_NAME: &str = /// Stable processor name persisted by metadata projections. pub(crate) const MT_METADATA_METAPLEX_TOKEN_METADATA_PROCESSOR_NAME: &str = "materializer.metadata.metaplex_token_metadata"; +/// Event families accepted by the Metaplex Token Metadata instruction materializer. +pub(crate) const MT_METADATA_METAPLEX_TOKEN_METADATA_ACCEPTED_FAMILIES: &[crate::MdEventFamily] = + &[crate::MdEventFamily::Metadata]; /// Stable projection contract version. pub(crate) const MT_METADATA_METAPLEX_TOKEN_METADATA_PROJECTION_VERSION: u32 = 2; /// Canonical tracing target for the metadata metaplex_token_metadata materializer. diff --git a/kb-lib/src/materializer/metadata/metaplex_token_metadata/materializer.rs b/kb-lib/src/materializer/metadata/metaplex_token_metadata/materializer.rs new file mode 100644 index 0000000..d4e4aff --- /dev/null +++ b/kb-lib/src/materializer/metadata/metaplex_token_metadata/materializer.rs @@ -0,0 +1,234 @@ +// file: kb-lib/src/materializer/metadata/metaplex_token_metadata/materializer.rs +// version: 1 + +//! Metaplex Token Metadata instruction materialization. + +/// Metaplex Token Metadata instruction materializer. +#[derive(Clone, Debug, Default)] +pub struct MtMetadataMetaplexTokenMetadataMaterializer; + +impl crate::MtMaterializer for crate::MtMetadataMetaplexTokenMetadataMaterializer { + fn materializer_name(&self) -> &'static str { + return crate::MT_METADATA_METAPLEX_TOKEN_METADATA_COMPONENT_NAME; + } + + fn materializer_version(&self) -> &'static str { + return env!("CARGO_PKG_VERSION"); + } + + fn accepts_event(&self, event: &crate::MdDecodedProtocolEvent) -> bool { + return event.event_family == crate::MdEventFamily::Metadata + && event.program_id.0 == kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID + && event.surface_code.0 == crate::DC_METADATA_MTM_SURFACE_CODE + && instruction_contract(event.event_name.0.as_str()).is_some(); + } + + fn materialize_event( + &self, + event: &crate::MdDecodedProtocolEvent, + ) -> kb_core::Result> { + if !crate::MtMaterializer::accepts_event(self, event) { + return std::result::Result::Ok(std::vec::Vec::new()); + } + return std::result::Result::Ok(std::vec![crate::MdMaterializedEvent { + signature: event.signature.clone(), + slot: event.slot, + decoded_family: event.event_family, + materialized_family: crate::MdMaterializedEventFamily::Metadata, + }]); + } +} + +impl crate::MtApiEventMaterializer for crate::MtMetadataMetaplexTokenMetadataMaterializer { + fn identity(&self) -> crate::MtApiMaterializerIdentity { + return crate::MtApiMaterializerIdentity { + name: crate::MT_METADATA_METAPLEX_TOKEN_METADATA_PROCESSOR_NAME.to_string(), + version: env!("CARGO_PKG_VERSION").to_string(), + }; + } + + fn accepted_families(&self) -> &'static [crate::MdEventFamily] { + return crate::MT_METADATA_METAPLEX_TOKEN_METADATA_ACCEPTED_FAMILIES; + } + + fn accepts_observation(&self, observation: &crate::DcApiDecodedObservation) -> bool { + return observation.event.event_family == crate::MdEventFamily::Metadata + && observation.event.program_id.0 + == kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID + && observation.event.surface_code.0 == crate::DC_METADATA_MTM_SURFACE_CODE + && instruction_contract(observation.event.event_name.0.as_str()).is_some() + && payload_contract_matches(observation); + } + + fn transaction_policy( + &self, + _family: crate::MdEventFamily, + ) -> crate::MtApiMaterializationTransactionPolicy { + return crate::MtApiMaterializationTransactionPolicy::SuccessfulCommittedOnly; + } + + fn materialize( + &self, + observation: &crate::DcApiDecodedObservation, + ) -> crate::MtApiMaterializerExecutionResult { + if !crate::MtApiEventMaterializer::accepts_observation(self, observation) { + return crate::MtApiMaterializerExecutionResult::ignored(); + } + if observation.transaction_failed || !observation.observation_committed { + return crate::MtApiMaterializerExecutionResult::refused( + "failed_transaction_metaplex_token_metadata_refused", + "failed or uncommitted Metaplex Token Metadata instructions cannot create metadata outputs", + ); + } + let operation = observation.event.event_name.0.as_str(); + let (_, historical) = match instruction_contract(operation) { + std::option::Option::Some(value) => value, + std::option::Option::None => return crate::MtApiMaterializerExecutionResult::ignored(), + }; + let deprecated = operation.starts_with("deprecated_"); + let output = crate::MtApiMaterializedOutput { + output_key: format!( + "metaplex_token_metadata_instruction:{}:{}:{}", + observation.event.signature.0, observation.event.instruction_path.0, operation + ), + family: crate::MdMaterializedEventFamily::Metadata, + payload_json: serde_json::json!({ + "projectionVersion": crate::MT_METADATA_METAPLEX_TOKEN_METADATA_PROJECTION_VERSION, + "domain": "metaplex_token_metadata", + "projectionKind": "committed_instruction_fact", + "projectionSemantics": "committed_instruction_fact_non_authoritative_state", + "operation": operation, + "programId": observation.event.program_id.0.clone(), + "signature": observation.event.signature.0.clone(), + "slot": observation.event.slot.0, + "instructionPath": observation.event.instruction_path.0.clone(), + "sourceKind": observation.event.source_kind, + "transactionSucceeded": true, + "historical": historical, + "deprecated": deprecated, + "authoritativeFinalStateCaptured": false, + "stateProjectionAuthority": "post_execution_account_snapshot", + "parameters": observation.payload_json.get("parameters").cloned().unwrap_or(serde_json::Value::Null), + "accounts": observation.payload_json.get("accounts").cloned().unwrap_or(serde_json::Value::Null), + "decodedPayload": observation.payload_json.clone(), + "provenance": { + "processorName": crate::MT_METADATA_METAPLEX_TOKEN_METADATA_PROCESSOR_NAME, + "processorVersion": env!("CARGO_PKG_VERSION"), + "decoderProof": observation.proof.clone() + } + }), + }; + tracing::debug!( + target: crate::TRACING_TARGET_MATERIALIZER_METADATA_METAPLEX_TOKEN_METADATA, + component_name = crate::MT_METADATA_METAPLEX_TOKEN_METADATA_COMPONENT_NAME, + processor_name = crate::MT_METADATA_METAPLEX_TOKEN_METADATA_PROCESSOR_NAME, + action = "materialize_metaplex_token_metadata_instruction", + operation = operation, + historical = historical, + deprecated = deprecated, + signature = %observation.event.signature.0, + instruction_path = %observation.event.instruction_path.0, + output_count = 1_usize, + "materialize committed Metaplex Token Metadata instruction" + ); + return crate::MtApiMaterializerExecutionResult { + status: crate::MtApiMaterializerOutcomeStatus::Inserted, + outputs: std::vec![output], + diagnostics: std::vec::Vec::new(), + }; + } +} + +fn instruction_contract(operation: &str) -> std::option::Option<(u8, bool)> { + return crate::DC_METADATA_MTM_INSTRUCTION_ENTRIES + .iter() + .find(|(_, code, _)| return *code == operation) + .map(|(discriminator, _, historical)| return (*discriminator, *historical)); +} + +fn payload_contract_matches(observation: &crate::DcApiDecodedObservation) -> bool { + let instruction = + observation.payload_json.get("instruction").and_then(serde_json::Value::as_str); + let program_id = observation.payload_json.get("programId").and_then(serde_json::Value::as_str); + return instruction == std::option::Option::Some(observation.event.event_name.0.as_str()) + && program_id + == std::option::Option::Some( + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + ); +} + +#[cfg(test)] +mod tests { + fn observation(operation: &str, committed: bool) -> crate::DcApiDecodedObservation { + return crate::DcApiDecodedObservation { + event_key: format!("metaplex_token_metadata:{operation}:0"), + event: crate::MdDecodedProtocolEvent { + signature: crate::MdSignature("signature111".to_string()), + slot: crate::MdSlot(42), + instruction_path: crate::MdInstructionPath("0".to_string()), + program_id: crate::MdProgramId( + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID.to_string(), + ), + protocol_code: crate::MdProtocolCode( + crate::DC_METADATA_MTM_SURFACE_CODE.to_string(), + ), + surface_code: crate::MdSurfaceCode(crate::DC_METADATA_MTM_SURFACE_CODE.to_string()), + event_code: crate::MdEventCode(format!( + "{}.{}", + crate::DC_METADATA_MTM_SURFACE_CODE, + operation + )), + event_name: crate::MdEventName(operation.to_string()), + event_family: crate::MdEventFamily::Metadata, + source_kind: crate::MdEventSourceKind::Instruction, + confidence: crate::MdDecoderConfidence::ManualExact, + }, + payload_json: serde_json::json!({ + "programId": kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + "instruction": operation, + "parameters": {}, + "accounts": [] + }), + transaction_failed: !committed, + transaction_error: std::option::Option::None, + observation_committed: committed, + proof: crate::DcApiDecoderProof { + kind: crate::DcApiDecoderProofKind::Manual, + confidence: crate::MdDecoderConfidence::ManualExact, + evidence: std::vec!["wire".to_string()], + }, + }; + } + + #[test] + fn every_decoded_instruction_has_one_metadata_owner() { + let materializer = crate::MtMetadataMetaplexTokenMetadataMaterializer; + for (_, operation, historical) in crate::DC_METADATA_MTM_INSTRUCTION_ENTRIES.iter().copied() + { + let value = observation(operation, true); + assert!(crate::MtApiEventMaterializer::accepts_observation(&materializer, &value)); + let result = crate::MtApiEventMaterializer::materialize(&materializer, &value); + assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted); + assert_eq!(result.outputs.len(), 1); + assert_eq!(result.outputs[0].payload_json["operation"], operation); + assert_eq!(result.outputs[0].payload_json["historical"], historical); + assert_eq!( + result.outputs[0].payload_json["deprecated"], + operation.starts_with("deprecated_") + ); + } + } + + #[test] + fn failed_and_foreign_observations_do_not_create_outputs() { + let materializer = crate::MtMetadataMetaplexTokenMetadataMaterializer; + let failed = observation("create_metadata_account_v3", false); + let refused = crate::MtApiEventMaterializer::materialize(&materializer, &failed); + assert_eq!(refused.status, crate::MtApiMaterializerOutcomeStatus::Refused); + let mut foreign = observation("create_metadata_account_v3", true); + foreign.event.program_id = + crate::MdProgramId(kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID.to_string()); + let ignored = crate::MtApiEventMaterializer::materialize(&materializer, &foreign); + assert_eq!(ignored.status, crate::MtApiMaterializerOutcomeStatus::Ignored); + } +} diff --git a/kb-lib/src/materializer/metadata/solana_program_metadata.rs b/kb-lib/src/materializer/metadata/solana_program_metadata.rs index f434e42..9033a90 100644 --- a/kb-lib/src/materializer/metadata/solana_program_metadata.rs +++ b/kb-lib/src/materializer/metadata/solana_program_metadata.rs @@ -1,5 +1,5 @@ // file: kb-lib/src/materializer/metadata/solana_program_metadata.rs -// version: 2 +// version: 3 //! Solana Program Metadata materialization surface. @@ -7,10 +7,8 @@ mod constants; mod materializer; mod state; -/// Compatibility materializer retaining the historical generic metadata type name. -pub use self::materializer::MtMetadataMaterializer; -/// Stable Solana Program Metadata instruction materializer. -pub use self::materializer::MtSolanaProgramMetadataMaterializer; +/// Solana Program Metadata instruction materializer. +pub use self::materializer::MtMetadataSolanaProgramMaterializer; /// Materializes one decoded Solana Program Metadata account snapshot. pub use self::state::materializer_metadata_materialize_solana_program_metadata_account_snapshot; /// Materializes one bounded Solana Program Metadata Buffer account snapshot. @@ -19,12 +17,12 @@ pub use self::state::materializer_metadata_materialize_solana_program_metadata_b pub use self::state::materializer_metadata_materialize_solana_program_metadata_metadata_snapshot; /// Event families accepted by the Solana Program Metadata instruction materializer. -pub(crate) use self::constants::MT_METADATA_SOLANA_PROGRAM_METADATA_ACCEPTED_FAMILIES; +pub(crate) use self::constants::MT_METADATA_SOLANA_PROGRAM_ACCEPTED_FAMILIES; /// Stable runtime component name for Solana Program Metadata projections. -pub(crate) use self::constants::MT_METADATA_SOLANA_PROGRAM_METADATA_COMPONENT_NAME; +pub(crate) use self::constants::MT_METADATA_SOLANA_PROGRAM_COMPONENT_NAME; /// Stable processor name for Solana Program Metadata projections. -pub(crate) use self::constants::MT_METADATA_SOLANA_PROGRAM_METADATA_PROCESSOR_NAME; +pub(crate) use self::constants::MT_METADATA_SOLANA_PROGRAM_PROCESSOR_NAME; /// Stable projection schema version for Solana Program Metadata outputs. -pub(crate) use self::constants::MT_METADATA_SOLANA_PROGRAM_METADATA_PROJECTION_VERSION; +pub(crate) use self::constants::MT_METADATA_SOLANA_PROGRAM_PROJECTION_VERSION; /// Canonical tracing target for Solana Program Metadata materialization. -pub(crate) use self::constants::TRACING_TARGET_MATERIALIZER_METADATA_SOLANA_PROGRAM_METADATA; +pub(crate) use self::constants::TRACING_TARGET_MATERIALIZER_METADATA_SOLANA_PROGRAM; diff --git a/kb-lib/src/materializer/metadata/solana_program_metadata/constants.rs b/kb-lib/src/materializer/metadata/solana_program_metadata/constants.rs index 372ef30..9119b46 100644 --- a/kb-lib/src/materializer/metadata/solana_program_metadata/constants.rs +++ b/kb-lib/src/materializer/metadata/solana_program_metadata/constants.rs @@ -4,15 +4,15 @@ //! Local constants for the Solana Program Metadata materializer. /// Stable runtime component name for Solana Program Metadata materialization. -pub(crate) const MT_METADATA_SOLANA_PROGRAM_METADATA_COMPONENT_NAME: &str = +pub(crate) const MT_METADATA_SOLANA_PROGRAM_COMPONENT_NAME: &str = "kb-lib.materializer.metadata.solana_program_metadata"; /// Stable processor name persisted by Solana Program Metadata projections. -pub(crate) const MT_METADATA_SOLANA_PROGRAM_METADATA_PROCESSOR_NAME: &str = +pub(crate) const MT_METADATA_SOLANA_PROGRAM_PROCESSOR_NAME: &str = "materializer.metadata.solana_program_metadata"; /// Stable projection schema version for Solana Program Metadata outputs. -pub(crate) const MT_METADATA_SOLANA_PROGRAM_METADATA_PROJECTION_VERSION: u32 = 1; -pub(crate) const MT_METADATA_SOLANA_PROGRAM_METADATA_ACCEPTED_FAMILIES: &[crate::MdEventFamily] = +pub(crate) const MT_METADATA_SOLANA_PROGRAM_PROJECTION_VERSION: u32 = 1; +pub(crate) const MT_METADATA_SOLANA_PROGRAM_ACCEPTED_FAMILIES: &[crate::MdEventFamily] = &[crate::MdEventFamily::Metadata]; /// Canonical tracing target for Solana Program Metadata materialization. -pub(crate) const TRACING_TARGET_MATERIALIZER_METADATA_SOLANA_PROGRAM_METADATA: &str = +pub(crate) const TRACING_TARGET_MATERIALIZER_METADATA_SOLANA_PROGRAM: &str = "kb-lib.materializer.metadata.solana_program_metadata"; diff --git a/kb-lib/src/materializer/metadata/solana_program_metadata/materializer.rs b/kb-lib/src/materializer/metadata/solana_program_metadata/materializer.rs index a7ed360..bbf0b52 100644 --- a/kb-lib/src/materializer/metadata/solana_program_metadata/materializer.rs +++ b/kb-lib/src/materializer/metadata/solana_program_metadata/materializer.rs @@ -1,19 +1,15 @@ // file: kb-lib/src/materializer/metadata/solana_program_metadata/materializer.rs -// version: 4 +// version: 5 //! Committed Solana Program Metadata instruction projections. /// Stable Solana Program Metadata instruction materializer. #[derive(Clone, Debug, Default)] -pub struct MtSolanaProgramMetadataMaterializer; +pub struct MtMetadataSolanaProgramMaterializer; -/// Compatibility materializer retaining the historical generic metadata type name. -#[derive(Clone, Debug, Default)] -pub struct MtMetadataMaterializer; - -impl crate::MtMaterializer for crate::MtSolanaProgramMetadataMaterializer { +impl crate::MtMaterializer for crate::MtMetadataSolanaProgramMaterializer { fn materializer_name(&self) -> &'static str { - return crate::MT_METADATA_SOLANA_PROGRAM_METADATA_COMPONENT_NAME; + return crate::MT_METADATA_SOLANA_PROGRAM_COMPONENT_NAME; } fn materializer_version(&self) -> &'static str { @@ -45,84 +41,16 @@ impl crate::MtMaterializer for crate::MtSolanaProgramMetadataMaterializer { } } -impl crate::MtMaterializer for crate::MtMetadataMaterializer { - fn materializer_name(&self) -> &'static str { - return crate::MT_METADATA_SOLANA_PROGRAM_METADATA_COMPONENT_NAME; - } - - fn materializer_version(&self) -> &'static str { - return env!("CARGO_PKG_VERSION"); - } - - fn accepts_event(&self, event: &crate::MdDecodedProtocolEvent) -> bool { - return crate::MtMaterializer::accepts_event( - &crate::MtSolanaProgramMetadataMaterializer, - event, - ); - } - - fn materialize_event( - &self, - event: &crate::MdDecodedProtocolEvent, - ) -> kb_core::Result> { - return crate::MtMaterializer::materialize_event( - &crate::MtSolanaProgramMetadataMaterializer, - event, - ); - } -} - -impl crate::MtApiEventMaterializer for crate::MtMetadataMaterializer { - fn identity(&self) -> crate::MtApiMaterializerIdentity { - return crate::MtApiEventMaterializer::identity( - &crate::MtSolanaProgramMetadataMaterializer, - ); - } - - fn accepted_families(&self) -> &'static [crate::MdEventFamily] { - return crate::MtApiEventMaterializer::accepted_families( - &crate::MtSolanaProgramMetadataMaterializer, - ); - } - - fn accepts_observation(&self, observation: &crate::DcApiDecodedObservation) -> bool { - return crate::MtApiEventMaterializer::accepts_observation( - &crate::MtSolanaProgramMetadataMaterializer, - observation, - ); - } - - fn transaction_policy( - &self, - family: crate::MdEventFamily, - ) -> crate::MtApiMaterializationTransactionPolicy { - return crate::MtApiEventMaterializer::transaction_policy( - &crate::MtSolanaProgramMetadataMaterializer, - family, - ); - } - - fn materialize( - &self, - observation: &crate::DcApiDecodedObservation, - ) -> crate::MtApiMaterializerExecutionResult { - return crate::MtApiEventMaterializer::materialize( - &crate::MtSolanaProgramMetadataMaterializer, - observation, - ); - } -} - -impl crate::MtApiEventMaterializer for crate::MtSolanaProgramMetadataMaterializer { +impl crate::MtApiEventMaterializer for crate::MtMetadataSolanaProgramMaterializer { fn identity(&self) -> crate::MtApiMaterializerIdentity { return crate::MtApiMaterializerIdentity { - name: crate::MT_METADATA_SOLANA_PROGRAM_METADATA_PROCESSOR_NAME.to_string(), + name: crate::MT_METADATA_SOLANA_PROGRAM_PROCESSOR_NAME.to_string(), version: env!("CARGO_PKG_VERSION").to_string(), }; } fn accepted_families(&self) -> &'static [crate::MdEventFamily] { - return crate::MT_METADATA_SOLANA_PROGRAM_METADATA_ACCEPTED_FAMILIES; + return crate::MT_METADATA_SOLANA_PROGRAM_ACCEPTED_FAMILIES; } fn accepts_observation(&self, observation: &crate::DcApiDecodedObservation) -> bool { @@ -165,7 +93,7 @@ impl crate::MtApiEventMaterializer for crate::MtSolanaProgramMetadataMaterialize ), family: crate::MdMaterializedEventFamily::Metadata, payload_json: serde_json::json!({ - "projectionVersion": crate::MT_METADATA_SOLANA_PROGRAM_METADATA_PROJECTION_VERSION, + "projectionVersion": crate::MT_METADATA_SOLANA_PROGRAM_PROJECTION_VERSION, "domain": "solana_program_metadata", "projectionKind": "committed_instruction_mutation", "projectionSemantics": "committed_instruction_fact_non_authoritative_state", @@ -183,14 +111,14 @@ impl crate::MtApiEventMaterializer for crate::MtSolanaProgramMetadataMaterialize "externalContentFetched": false, "externalAccountRead": false, "provenance": { - "processorName": crate::MT_METADATA_SOLANA_PROGRAM_METADATA_PROCESSOR_NAME, + "processorName": crate::MT_METADATA_SOLANA_PROGRAM_PROCESSOR_NAME, "processorVersion": env!("CARGO_PKG_VERSION"), "decoderProof": observation.proof.clone() } }), }; tracing::debug!( - target: crate::TRACING_TARGET_MATERIALIZER_METADATA_SOLANA_PROGRAM_METADATA, + target: crate::TRACING_TARGET_MATERIALIZER_METADATA_SOLANA_PROGRAM, action = "materialize_solana_program_metadata_instruction", operation = operation, signature = %observation.event.signature.0, @@ -355,9 +283,9 @@ mod tests { #[test] fn every_stable_instruction_has_one_materialized_mutation_fact() { - let materializer = crate::MtSolanaProgramMetadataMaterializer; + let materializer = crate::MtMetadataSolanaProgramMaterializer; let identity = crate::MtApiEventMaterializer::identity(&materializer); - assert_eq!(identity.name, crate::MT_METADATA_SOLANA_PROGRAM_METADATA_PROCESSOR_NAME); + assert_eq!(identity.name, crate::MT_METADATA_SOLANA_PROGRAM_PROCESSOR_NAME); for (_, operation) in crate::DC_METADATA_SPM_INSTRUCTION_ENTRIES.iter().copied() { let value = observation(operation, true); assert!(crate::MtApiEventMaterializer::accepts_observation(&materializer, &value)); @@ -369,14 +297,14 @@ mod tests { assert_eq!(result.outputs[0].payload_json["projection"]["replaced"], false); assert_eq!( result.outputs[0].payload_json["provenance"]["processorName"], - crate::MT_METADATA_SOLANA_PROGRAM_METADATA_PROCESSOR_NAME + crate::MT_METADATA_SOLANA_PROGRAM_PROCESSOR_NAME ); } } #[test] fn failed_instruction_is_refused_and_foreign_surface_is_ignored() { - let materializer = crate::MtSolanaProgramMetadataMaterializer; + let materializer = crate::MtMetadataSolanaProgramMaterializer; let failed = observation("set_data", false); let failure = crate::MtApiEventMaterializer::materialize(&materializer, &failed); assert_eq!(failure.status, crate::MtApiMaterializerOutcomeStatus::Refused); @@ -424,7 +352,7 @@ mod tests { #[test] fn trim_keeps_prestable_alias_as_provenance_only() { - let materializer = crate::MtSolanaProgramMetadataMaterializer; + let materializer = crate::MtMetadataSolanaProgramMaterializer; let value = observation("trim", true); let result = crate::MtApiEventMaterializer::materialize(&materializer, &value); assert_eq!( diff --git a/kb-lib/src/materializer/metadata/solana_program_metadata/state.rs b/kb-lib/src/materializer/metadata/solana_program_metadata/state.rs index 7c0c92d..7f5dc06 100644 --- a/kb-lib/src/materializer/metadata/solana_program_metadata/state.rs +++ b/kb-lib/src/materializer/metadata/solana_program_metadata/state.rs @@ -63,7 +63,7 @@ pub fn materializer_metadata_materialize_solana_program_metadata_buffer_snapshot output_key: format!("solana_program_metadata_account_state:{}", snapshot.account), family: crate::MdMaterializedEventFamily::Metadata, payload_json: serde_json::json!({ - "projectionVersion": crate::MT_METADATA_SOLANA_PROGRAM_METADATA_PROJECTION_VERSION, + "projectionVersion": crate::MT_METADATA_SOLANA_PROGRAM_PROJECTION_VERSION, "domain": "solana_program_metadata", "projectionKind": "buffer_state", "projectionSemantics": "authoritative_bounded_owned_account_snapshot", @@ -83,7 +83,7 @@ pub fn materializer_metadata_materialize_solana_program_metadata_buffer_snapshot "externalAccountRead": false, "instructionProjectionRole": "mutation_observation_only_no_duplicate_state", "provenance": { - "processorName": crate::MT_METADATA_SOLANA_PROGRAM_METADATA_PROCESSOR_NAME, + "processorName": crate::MT_METADATA_SOLANA_PROGRAM_PROCESSOR_NAME, "processorVersion": env!("CARGO_PKG_VERSION"), "source": "bounded_solana_program_metadata_account_decoder" } @@ -150,7 +150,7 @@ pub fn materializer_metadata_materialize_solana_program_metadata_metadata_snapsh output_key: format!("solana_program_metadata_account_state:{}", snapshot.account), family: crate::MdMaterializedEventFamily::Metadata, payload_json: serde_json::json!({ - "projectionVersion": crate::MT_METADATA_SOLANA_PROGRAM_METADATA_PROJECTION_VERSION, + "projectionVersion": crate::MT_METADATA_SOLANA_PROGRAM_PROJECTION_VERSION, "domain": "solana_program_metadata", "projectionKind": "metadata_state", "projectionSemantics": "authoritative_bounded_owned_account_snapshot", @@ -177,7 +177,7 @@ pub fn materializer_metadata_materialize_solana_program_metadata_metadata_snapsh "externalAccountRead": false, "instructionProjectionRole": "mutation_observation_only_no_duplicate_state", "provenance": { - "processorName": crate::MT_METADATA_SOLANA_PROGRAM_METADATA_PROCESSOR_NAME, + "processorName": crate::MT_METADATA_SOLANA_PROGRAM_PROCESSOR_NAME, "processorVersion": env!("CARGO_PKG_VERSION"), "source": "bounded_solana_program_metadata_account_decoder" } diff --git a/kb-lib/src/materializer/metadata/token_2022.rs b/kb-lib/src/materializer/metadata/token_2022.rs index a315741..445f7e7 100644 --- a/kb-lib/src/materializer/metadata/token_2022.rs +++ b/kb-lib/src/materializer/metadata/token_2022.rs @@ -1,16 +1,23 @@ // file: kb-lib/src/materializer/metadata/token_2022.rs -// version: 2 +// version: 3 //! Embedded SPL Token-2022 metadata materialization surface. mod constants; +mod materializer; mod state; +/// SPL Token-2022 embedded metadata and group instruction materializer. +pub use self::materializer::MtMetadataToken2022Materializer; /// Materializes Token-2022 embedded metadata and group snapshots. pub use self::state::materializer_metadata_materialize_token_2022_snapshot; +/// Event families accepted by the Token-2022 metadata instruction materializer. +pub(crate) use self::constants::MT_METADATA_TOKEN_2022_ACCEPTED_FAMILIES; /// Stable runtime component name for Token-2022 embedded metadata projections. pub(crate) use self::constants::MT_METADATA_TOKEN_2022_COMPONENT_NAME; +/// Exact embedded Token Metadata and Token Group instruction ownership. +pub(crate) use self::constants::MT_METADATA_TOKEN_2022_INSTRUCTION_ENTRIES; /// Stable processor name for Token-2022 embedded metadata projections. pub(crate) use self::constants::MT_METADATA_TOKEN_2022_PROCESSOR_NAME; /// Stable Token-2022 embedded metadata projection version. diff --git a/kb-lib/src/materializer/metadata/token_2022/constants.rs b/kb-lib/src/materializer/metadata/token_2022/constants.rs index f637b61..f453887 100644 --- a/kb-lib/src/materializer/metadata/token_2022/constants.rs +++ b/kb-lib/src/materializer/metadata/token_2022/constants.rs @@ -1,5 +1,5 @@ // file: kb-lib/src/materializer/metadata/token_2022/constants.rs -// version: 3 +// version: 4 //! Local constants for the metadata token_2022 materializer. @@ -8,6 +8,21 @@ pub(crate) const MT_METADATA_TOKEN_2022_COMPONENT_NAME: &str = "kb-lib.materializer.metadata.token_2022"; /// Stable processor name persisted by metadata projections. pub(crate) const MT_METADATA_TOKEN_2022_PROCESSOR_NAME: &str = "materializer.metadata.token_2022"; +/// Event families accepted by the Token-2022 metadata instruction materializer. +pub(crate) const MT_METADATA_TOKEN_2022_ACCEPTED_FAMILIES: &[crate::MdEventFamily] = + &[crate::MdEventFamily::Metadata]; +/// Exact embedded Token Metadata and Token Group instruction ownership. +pub(crate) const MT_METADATA_TOKEN_2022_INSTRUCTION_ENTRIES: &[&str] = &[ + "initialize_token_metadata", + "update_token_metadata_field", + "remove_token_metadata_key", + "update_token_metadata_authority", + "emit_token_metadata", + "initialize_token_group", + "update_token_group_max_size", + "update_token_group_authority", + "initialize_token_group_member", +]; /// Stable projection contract version. pub(crate) const MT_METADATA_TOKEN_2022_PROJECTION_VERSION: u32 = 2; /// Canonical tracing target for the metadata token_2022 materializer. diff --git a/kb-lib/src/materializer/metadata/token_2022/materializer.rs b/kb-lib/src/materializer/metadata/token_2022/materializer.rs new file mode 100644 index 0000000..493fd4c --- /dev/null +++ b/kb-lib/src/materializer/metadata/token_2022/materializer.rs @@ -0,0 +1,232 @@ +// file: kb-lib/src/materializer/metadata/token_2022/materializer.rs +// version: 1 + +//! SPL Token-2022 embedded metadata and group instruction materialization. + +/// SPL Token-2022 embedded metadata and group instruction materializer. +#[derive(Clone, Debug, Default)] +pub struct MtMetadataToken2022Materializer; + +impl crate::MtMaterializer for crate::MtMetadataToken2022Materializer { + fn materializer_name(&self) -> &'static str { + return crate::MT_METADATA_TOKEN_2022_COMPONENT_NAME; + } + + fn materializer_version(&self) -> &'static str { + return env!("CARGO_PKG_VERSION"); + } + + fn accepts_event(&self, event: &crate::MdDecodedProtocolEvent) -> bool { + return event.event_family == crate::MdEventFamily::Metadata + && event.program_id.0 == kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID + && event.surface_code.0 == crate::DC_SPL_TOKEN_2022_SURFACE_CODE + && crate::MT_METADATA_TOKEN_2022_INSTRUCTION_ENTRIES + .iter() + .any(|code| return *code == event.event_name.0.as_str()); + } + + fn materialize_event( + &self, + event: &crate::MdDecodedProtocolEvent, + ) -> kb_core::Result> { + if !crate::MtMaterializer::accepts_event(self, event) { + return std::result::Result::Ok(std::vec::Vec::new()); + } + return std::result::Result::Ok(std::vec![crate::MdMaterializedEvent { + signature: event.signature.clone(), + slot: event.slot, + decoded_family: event.event_family, + materialized_family: crate::MdMaterializedEventFamily::Metadata, + }]); + } +} + +impl crate::MtApiEventMaterializer for crate::MtMetadataToken2022Materializer { + fn identity(&self) -> crate::MtApiMaterializerIdentity { + return crate::MtApiMaterializerIdentity { + name: crate::MT_METADATA_TOKEN_2022_PROCESSOR_NAME.to_string(), + version: env!("CARGO_PKG_VERSION").to_string(), + }; + } + + fn accepted_families(&self) -> &'static [crate::MdEventFamily] { + return crate::MT_METADATA_TOKEN_2022_ACCEPTED_FAMILIES; + } + + fn accepts_observation(&self, observation: &crate::DcApiDecodedObservation) -> bool { + return observation.event.event_family == crate::MdEventFamily::Metadata + && observation.event.program_id.0 == kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID + && observation.event.surface_code.0 == crate::DC_SPL_TOKEN_2022_SURFACE_CODE + && crate::MT_METADATA_TOKEN_2022_INSTRUCTION_ENTRIES + .iter() + .any(|code| return *code == observation.event.event_name.0.as_str()) + && payload_contract_matches(observation); + } + + fn transaction_policy( + &self, + _family: crate::MdEventFamily, + ) -> crate::MtApiMaterializationTransactionPolicy { + return crate::MtApiMaterializationTransactionPolicy::SuccessfulCommittedOnly; + } + + fn materialize( + &self, + observation: &crate::DcApiDecodedObservation, + ) -> crate::MtApiMaterializerExecutionResult { + if !crate::MtApiEventMaterializer::accepts_observation(self, observation) { + return crate::MtApiMaterializerExecutionResult::ignored(); + } + if observation.transaction_failed || !observation.observation_committed { + return crate::MtApiMaterializerExecutionResult::refused( + "failed_transaction_token_2022_metadata_refused", + "failed or uncommitted Token-2022 metadata instructions cannot create metadata outputs", + ); + } + let operation = observation.event.event_name.0.as_str(); + let output = crate::MtApiMaterializedOutput { + output_key: format!( + "token_2022_metadata_instruction:{}:{}:{}", + observation.event.signature.0, observation.event.instruction_path.0, operation + ), + family: crate::MdMaterializedEventFamily::Metadata, + payload_json: serde_json::json!({ + "projectionVersion": crate::MT_METADATA_TOKEN_2022_PROJECTION_VERSION, + "domain": "token_2022_metadata", + "projectionKind": projection_kind(operation), + "projectionSemantics": "committed_instruction_fact_non_authoritative_state", + "operation": operation, + "programId": observation.event.program_id.0.clone(), + "signature": observation.event.signature.0.clone(), + "slot": observation.event.slot.0, + "instructionPath": observation.event.instruction_path.0.clone(), + "sourceKind": observation.event.source_kind, + "transactionSucceeded": true, + "authoritativeFinalStateCaptured": false, + "stateProjectionAuthority": "post_execution_token_2022_mint_snapshot", + "parameters": observation.payload_json.get("parameters").cloned().unwrap_or(serde_json::Value::Null), + "accounts": observation.payload_json.get("accounts").cloned().unwrap_or(serde_json::Value::Null), + "authority": observation.payload_json.get("authority").cloned().unwrap_or(serde_json::Value::Null), + "decodedPayload": observation.payload_json.clone(), + "provenance": { + "processorName": crate::MT_METADATA_TOKEN_2022_PROCESSOR_NAME, + "processorVersion": env!("CARGO_PKG_VERSION"), + "decoderProof": observation.proof.clone() + } + }), + }; + tracing::debug!( + target: crate::TRACING_TARGET_MATERIALIZER_METADATA_TOKEN_2022, + component_name = crate::MT_METADATA_TOKEN_2022_COMPONENT_NAME, + processor_name = crate::MT_METADATA_TOKEN_2022_PROCESSOR_NAME, + action = "materialize_token_2022_metadata_instruction", + operation = operation, + signature = %observation.event.signature.0, + instruction_path = %observation.event.instruction_path.0, + output_count = 1_usize, + "materialize committed Token-2022 metadata instruction" + ); + return crate::MtApiMaterializerExecutionResult { + status: crate::MtApiMaterializerOutcomeStatus::Inserted, + outputs: std::vec![output], + diagnostics: std::vec::Vec::new(), + }; + } +} + +fn payload_contract_matches(observation: &crate::DcApiDecodedObservation) -> bool { + let instruction = + observation.payload_json.get("instruction").and_then(serde_json::Value::as_str); + let program_id = observation.payload_json.get("programId").and_then(serde_json::Value::as_str); + return instruction == std::option::Option::Some(observation.event.event_name.0.as_str()) + && program_id == std::option::Option::Some(kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID); +} + +fn projection_kind(operation: &str) -> &'static str { + return match operation { + "initialize_token_metadata" => "metadata_initialized", + "update_token_metadata_field" => "metadata_field_updated", + "remove_token_metadata_key" => "metadata_key_removed", + "update_token_metadata_authority" => "metadata_authority_updated", + "emit_token_metadata" => "metadata_emitted", + "initialize_token_group" => "token_group_initialized", + "update_token_group_max_size" => "token_group_max_size_updated", + "update_token_group_authority" => "token_group_authority_updated", + "initialize_token_group_member" => "token_group_member_initialized", + _ => "unknown", + }; +} + +#[cfg(test)] +mod tests { + fn observation(operation: &str, committed: bool) -> crate::DcApiDecodedObservation { + return crate::DcApiDecodedObservation { + event_key: format!("token_2022:{operation}:0"), + event: crate::MdDecodedProtocolEvent { + signature: crate::MdSignature("signature111".to_string()), + slot: crate::MdSlot(42), + instruction_path: crate::MdInstructionPath("0".to_string()), + program_id: crate::MdProgramId( + kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID.to_string(), + ), + protocol_code: crate::MdProtocolCode( + crate::DC_SPL_TOKEN_2022_SURFACE_CODE.to_string(), + ), + surface_code: crate::MdSurfaceCode( + crate::DC_SPL_TOKEN_2022_SURFACE_CODE.to_string(), + ), + event_code: crate::MdEventCode(format!( + "{}.{}", + crate::DC_SPL_TOKEN_2022_SURFACE_CODE, + operation + )), + event_name: crate::MdEventName(operation.to_string()), + event_family: crate::MdEventFamily::Metadata, + source_kind: crate::MdEventSourceKind::Instruction, + confidence: crate::MdDecoderConfidence::ManualExact, + }, + payload_json: serde_json::json!({ + "programId": kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID, + "instruction": operation, + "parameters": {}, + "accounts": [], + "authority": null + }), + transaction_failed: !committed, + transaction_error: std::option::Option::None, + observation_committed: committed, + proof: crate::DcApiDecoderProof { + kind: crate::DcApiDecoderProofKind::Manual, + confidence: crate::MdDecoderConfidence::ManualExact, + evidence: std::vec!["wire".to_string()], + }, + }; + } + + #[test] + fn every_embedded_metadata_and_group_instruction_has_one_owner() { + let materializer = crate::MtMetadataToken2022Materializer; + for operation in crate::MT_METADATA_TOKEN_2022_INSTRUCTION_ENTRIES { + let value = observation(operation, true); + assert!(crate::MtApiEventMaterializer::accepts_observation(&materializer, &value)); + let result = crate::MtApiEventMaterializer::materialize(&materializer, &value); + assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted); + assert_eq!(result.outputs.len(), 1); + assert_eq!(result.outputs[0].payload_json["operation"], *operation); + } + } + + #[test] + fn failed_and_non_metadata_observations_do_not_create_outputs() { + let materializer = crate::MtMetadataToken2022Materializer; + let failed = observation("initialize_token_metadata", false); + let refused = crate::MtApiEventMaterializer::materialize(&materializer, &failed); + assert_eq!(refused.status, crate::MtApiMaterializerOutcomeStatus::Refused); + let mut unrelated = observation("initialize_token_metadata", true); + unrelated.event.event_name = crate::MdEventName("transfer_checked".to_string()); + unrelated.payload_json["instruction"] = + serde_json::Value::String("transfer_checked".to_string()); + let ignored = crate::MtApiEventMaterializer::materialize(&materializer, &unrelated); + assert_eq!(ignored.status, crate::MtApiMaterializerOutcomeStatus::Ignored); + } +} diff --git a/kb-lib/tests/external_materializer_api.rs b/kb-lib/tests/external_materializer_api.rs index c535e36..8f21498 100644 --- a/kb-lib/tests/external_materializer_api.rs +++ b/kb-lib/tests/external_materializer_api.rs @@ -1,5 +1,5 @@ // file: kb-lib/tests/external_materializer_api.rs -// version: 7 +// version: 8 //! Downstream-style compilation contract for externally implemented materializers. @@ -50,8 +50,9 @@ fn external_materializer_uses_only_the_public_kb_lib_contract() { assert_event_materializer::(); assert_event_materializer::(); assert_event_materializer::(); - assert_event_materializer::(); - assert_event_materializer::(); + assert_event_materializer::(); + assert_event_materializer::(); + assert_event_materializer::(); assert_event_materializer::(); assert_event_materializer::(); assert_event_materializer::(); @@ -71,8 +72,9 @@ fn external_materializer_uses_only_the_public_kb_lib_contract() { assert_legacy_materializer::(); assert_legacy_materializer::(); assert_legacy_materializer::(); - assert_legacy_materializer::(); - assert_legacy_materializer::(); + assert_legacy_materializer::(); + assert_legacy_materializer::(); + assert_legacy_materializer::(); assert_legacy_materializer::(); assert_legacy_materializer::(); assert_legacy_materializer::(); diff --git a/kb-pipeline/CHANGELOG.md b/kb-pipeline/CHANGELOG.md index 13c7ff7..64a70e4 100644 --- a/kb-pipeline/CHANGELOG.md +++ b/kb-pipeline/CHANGELOG.md @@ -1,8 +1,17 @@ - + # CHANGELOG — kb-pipeline +## 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 à `kb-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 `kb-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 d’API 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 ; diff --git a/kb-pipeline/README.md b/kb-pipeline/README.md index d7185fd..3bcac19 100644 --- a/kb-pipeline/README.md +++ b/kb-pipeline/README.md @@ -1,5 +1,5 @@ - + # kb-pipeline @@ -15,26 +15,34 @@ - lectures stateful et corrélations ; - préflights d’exécution ; - résolution des signers ; -- orchestration simulation-first, confirmation, soumission et postvalidation ; +- 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 l’agrégation des postconditions. Elle ne contient ni fixture Devnet, ni wallet de démonstration, ni séquence métier spécifique à un test. +Pour Metaplex Token Metadata, la crate expose les lectures stateful bornées, le préflight, la validation de readiness et l’agrégation des postconditions. + +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_account` ; +- `read_metaplex_token_metadata_stateful_snapshot` ; - `inspect_metaplex_token_metadata_preflight` ; - `validate_metaplex_token_metadata_execution_readiness` ; -- `summarize_metaplex_token_metadata_postconditions`. +- `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 `kb-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 d’API. Voir [USAGE.md](USAGE.md) pour les exemples et invariants. ## Relations avec le workspace -La crate coordonne `kb-onchain-transport`, `kb-store`, `kb-lib`, `kb-core` et `kb-program-ids`. Elle ne dépend jamais de `kb-pipeline-demo-scenarios` ni de `kb-app-demo-desktop`. +La crate coordonne `kb-onchain-transport`, `kb-store`, `kb-lib`, `kb-core` et `kb-program-ids`. Elle ne dépend jamais de `kb-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 diff --git a/kb-pipeline/TODO.md b/kb-pipeline/TODO.md index a14c793..dc6102d 100644 --- a/kb-pipeline/TODO.md +++ b/kb-pipeline/TODO.md @@ -1,8 +1,14 @@ - + # TODO — kb-pipeline +## Validation réseau Solana Program Metadata + +- [ ] `0.4.8-pre.009` - valider les neuf opérations sur Devnet depuis `kb-pipeline-demo-scenarios`, avec fixture native, simulation, soumission confirmée, lectures avant/après et replay. +- [ ] `0.4.8-pre.009` - vérifier depuis `demo_execution_metadata` que les scénarios desktop consomment les mêmes runners réutilisables sans dupliquer la logique du pipeline. +- [ ] Validation historique - exécuter `demo_backfill`, Core extraction et decode replay sur des signatures `ProgM6…` représentatives, puis contrôler les sorties matérialisées. + ## Réaudit ultérieur des surfaces historiques - [ ] Version à déterminer - après le réaudit des exécuteurs Solana Core et SPL dans `kb-lib`, vérifier que le pipeline orchestre sans filtrage implicite les opérations courantes et les opérations obsolètes explicitement approuvées. diff --git a/kb-pipeline/USAGE.md b/kb-pipeline/USAGE.md index 989d408..a604df4 100644 --- a/kb-pipeline/USAGE.md +++ b/kb-pipeline/USAGE.md @@ -1,5 +1,5 @@ - + # Utilisation de kb-pipeline @@ -152,6 +152,57 @@ where Les rapports stateful ne constituent jamais une autorisation implicite d’exé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. l’extraction 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. `kb-pipeline-demo-scenarios` n’intervient pas dans ce parcours historique. + +## Lire et prévalider Solana Program Metadata + +```rust +async fn read_program_metadata( + pool: &kb_onchain_transport::HttpEndpointPool, + account: kb_lib::MdPubkey, +) -> kb_core::Result { + let request = kb_pipeline::SolanaProgramMetadataStatefulReadRequest { + query_role: "rpc".to_string(), + account, + expected_state: kb_pipeline::SolanaProgramMetadataExpectedAccountState::Any, + min_context_slot: std::option::Option::None, + max_data_bytes: kb_pipeline::MAX_SOLANA_PROGRAM_METADATA_STATEFUL_ACCOUNT_BYTES, + }; + let result = kb_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), + }; +} +``` + +Le préflight reçoit l’intent 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 lorsqu’elles sont explicitement approuvées et que l’enveloppe commune de sécurité est satisfaite. + +Après simulation du message exact, `validate_solana_program_metadata_execution_readiness` vérifie les signers et l’autorisation 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 d’observation distincts pour le backfill, l’extraction Core et le replay. L’observateur peut publier la progression et participer à l’annulation coopérative selon le contrat concerné. diff --git a/kb-pipeline/src/lib.rs b/kb-pipeline/src/lib.rs index 3532cb1..971c998 100644 --- a/kb-pipeline/src/lib.rs +++ b/kb-pipeline/src/lib.rs @@ -1,5 +1,5 @@ // file: kb-pipeline/src/lib.rs -// version: 18 +// version: 20 #![forbid(unsafe_code)] #![deny(unreachable_pub)] @@ -11,20 +11,23 @@ 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_ata_stateful; -mod solana_elgamal_registry_stateful; -mod solana_metaplex_token_metadata_execution_orchestration; -mod solana_metaplex_token_metadata_preflight; -mod solana_metaplex_token_metadata_stateful; mod solana_stateful; -mod solana_token_2022_correlation; -mod solana_token_2022_crypto_preflight; -mod solana_token_2022_execution_orchestration; -mod solana_token_2022_preflight; -mod solana_token_2022_proof_orchestration; -mod solana_token_2022_stateful; -mod solana_token_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_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; @@ -90,74 +93,88 @@ pub use self::decode_replay::INSTRUCTION_DECODE_STAGE; 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 `kb-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; -/// Stateful invariants observed after one confirmed Associated Token Account execution. -pub use self::solana_ata_stateful::SplAssociatedTokenAccountPostExecutionReport; -/// One machine-readable Associated Token Account stateful check. -pub use self::solana_ata_stateful::SplAssociatedTokenAccountStatefulCheck; -/// One contextual Associated Token Account stateful fact. -pub use self::solana_ata_stateful::SplAssociatedTokenAccountStatefulFact; -/// Complete Associated Token Account stateful readiness report. -pub use self::solana_ata_stateful::SplAssociatedTokenAccountStatefulReadinessReport; -/// Complete request for one Associated Token Account stateful readiness inspection. -pub use self::solana_ata_stateful::SplAssociatedTokenAccountStatefulReadinessRequest; -/// Associated Token Account stateful readiness outcome. -pub use self::solana_ata_stateful::SplAssociatedTokenAccountStatefulReadinessStatus; -/// Verifies final Associated Token Account relationships after confirmed execution. -pub use self::solana_ata_stateful::inspect_spl_associated_token_account_post_execution; -/// Inspects state required before simulating one Associated Token Account operation. -pub use self::solana_ata_stateful::inspect_spl_associated_token_account_stateful_readiness; -/// Migrated ELGAMAL_REGISTRY_STATEFUL_ACCOUNT_BYTES contract. -pub use self::solana_elgamal_registry_stateful::ELGAMAL_REGISTRY_STATEFUL_ACCOUNT_BYTES; -/// Migrated ElGamalRegistryStatefulReadRequest contract. -pub use self::solana_elgamal_registry_stateful::ElGamalRegistryStatefulReadRequest; -/// Migrated ElGamalRegistryStatefulReadResult contract. -pub use self::solana_elgamal_registry_stateful::ElGamalRegistryStatefulReadResult; -/// Migrated ElGamalRegistryStatefulSnapshot contract. -pub use self::solana_elgamal_registry_stateful::ElGamalRegistryStatefulSnapshot; -/// Migrated materialize_elgamal_registry_account_info_result contract. -pub use self::solana_elgamal_registry_stateful::materialize_elgamal_registry_account_info_result; -/// Migrated materialize_elgamal_registry_stateful_snapshot contract. -pub use self::solana_elgamal_registry_stateful::materialize_elgamal_registry_stateful_snapshot; -/// Migrated read_elgamal_registry_stateful_snapshot contract. -pub use self::solana_elgamal_registry_stateful::read_elgamal_registry_stateful_snapshot; -/// Maximum number of resolved signers accepted by one Metaplex execution envelope. -pub use self::solana_metaplex_token_metadata_execution_orchestration::MAX_METAPLEX_TOKEN_METADATA_EXECUTION_SIGNERS; -/// Deterministic Metaplex execution-readiness report. -pub use self::solana_metaplex_token_metadata_execution_orchestration::MetaplexTokenMetadataExecutionReadinessReport; -/// Complete Metaplex execution-readiness request. -pub use self::solana_metaplex_token_metadata_execution_orchestration::MetaplexTokenMetadataExecutionReadinessRequest; -/// Result of one Metaplex stateful postcondition. -pub use self::solana_metaplex_token_metadata_execution_orchestration::MetaplexTokenMetadataPostconditionStatus; -/// Aggregates Metaplex postconditions without inventing success. -pub use self::solana_metaplex_token_metadata_execution_orchestration::summarize_metaplex_token_metadata_postconditions; -/// Validates the complete Metaplex execution envelope before signing. -pub use self::solana_metaplex_token_metadata_execution_orchestration::validate_metaplex_token_metadata_execution_readiness; -/// Maximum number of correlated Metaplex snapshots accepted by one preflight. -pub use self::solana_metaplex_token_metadata_preflight::MAX_METAPLEX_TOKEN_METADATA_PREFLIGHT_ACCOUNTS; -/// Deterministic Metaplex preflight report. -pub use self::solana_metaplex_token_metadata_preflight::MetaplexTokenMetadataPreflightReport; -/// Complete Metaplex preflight request. -pub use self::solana_metaplex_token_metadata_preflight::MetaplexTokenMetadataPreflightRequest; -/// Inspects one prepared Metaplex plan against confirmed state snapshots. -pub use self::solana_metaplex_token_metadata_preflight::inspect_metaplex_token_metadata_preflight; -/// Maximum complete Metaplex account data accepted by one RPC read. -pub use self::solana_metaplex_token_metadata_stateful::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES; -/// Supported Metaplex account category for one stateful read. -pub use self::solana_metaplex_token_metadata_stateful::MetaplexTokenMetadataAccountKind; -/// One bounded Metaplex stateful read request. -pub use self::solana_metaplex_token_metadata_stateful::MetaplexTokenMetadataStatefulReadRequest; -/// One bounded Metaplex stateful read result. -pub use self::solana_metaplex_token_metadata_stateful::MetaplexTokenMetadataStatefulReadResult; -/// Canonical bounded Metaplex state snapshot. -pub use self::solana_metaplex_token_metadata_stateful::MetaplexTokenMetadataStatefulSnapshot; -/// Validates one RPC account response and delegates parsing to `kb-lib`. -pub use self::solana_metaplex_token_metadata_stateful::materialize_metaplex_token_metadata_account_info_result; -/// Reads one bounded Metaplex Token Metadata account. -pub use self::solana_metaplex_token_metadata_stateful::read_metaplex_token_metadata_stateful_snapshot; /// One machine-readable native Solana stateful readiness check. pub use self::solana_stateful::SolanaCoreStatefulCheck; /// One contextual fact measured during native Solana stateful readiness. @@ -170,94 +187,124 @@ pub use self::solana_stateful::SolanaCoreStatefulReadinessRequest; 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::solana_token_2022_correlation::Token2022CorrelationStatus; +pub use self::spl_token_2022_correlation::Token2022CorrelationStatus; /// Deterministic correlation report for one instruction output and one final snapshot. -pub use self::solana_token_2022_correlation::Token2022StateCorrelationReport; +pub use self::spl_token_2022_correlation::Token2022StateCorrelationReport; /// Correlates one materialized instruction fact with one authoritative Token-2022 snapshot. -pub use self::solana_token_2022_correlation::correlate_token_2022_instruction_with_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::solana_token_2022_crypto_preflight::MAX_TOKEN_2022_PROOF_CONTEXTS; +pub use self::spl_token_2022_crypto_preflight::MAX_TOKEN_2022_PROOF_CONTEXTS; /// Token-2022 cryptographic preflight report. -pub use self::solana_token_2022_crypto_preflight::Token2022CryptographicPreflightReport; +pub use self::spl_token_2022_crypto_preflight::Token2022CryptographicPreflightReport; /// Token-2022 cryptographic preflight request. -pub use self::solana_token_2022_crypto_preflight::Token2022CryptographicPreflightRequest; +pub use self::spl_token_2022_crypto_preflight::Token2022CryptographicPreflightRequest; /// One validated proof context report. -pub use self::solana_token_2022_crypto_preflight::Token2022ProofContextReport; +pub use self::spl_token_2022_crypto_preflight::Token2022ProofContextReport; /// One required proof context-state account. -pub use self::solana_token_2022_crypto_preflight::Token2022ProofContextRequirement; +pub use self::spl_token_2022_crypto_preflight::Token2022ProofContextRequirement; /// Encoded proof context metadata bytes. -pub use self::solana_token_2022_crypto_preflight::ZK_PROOF_CONTEXT_META_BYTES; +pub use self::spl_token_2022_crypto_preflight::ZK_PROOF_CONTEXT_META_BYTES; /// Inspects bounded Token-2022 cryptographic proof contexts. -pub use self::solana_token_2022_crypto_preflight::inspect_token_2022_cryptographic_preflight; +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::solana_token_2022_execution_orchestration::MAX_TOKEN_2022_EXECUTION_SIGNERS; +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::solana_token_2022_execution_orchestration::Token2022ExecutionPostcondition; +pub use self::spl_token_2022_execution_orchestration::Token2022ExecutionPostcondition; /// Result of one stateful postcondition after Token-2022 execution. -pub use self::solana_token_2022_execution_orchestration::Token2022ExecutionPostconditionStatus; +pub use self::spl_token_2022_execution_orchestration::Token2022ExecutionPostconditionStatus; /// Deterministic Token-2022 execution-readiness report. -pub use self::solana_token_2022_execution_orchestration::Token2022ExecutionReadinessReport; +pub use self::spl_token_2022_execution_orchestration::Token2022ExecutionReadinessReport; /// Complete deterministic Token-2022 execution-readiness request. -pub use self::solana_token_2022_execution_orchestration::Token2022ExecutionReadinessRequest; +pub use self::spl_token_2022_execution_orchestration::Token2022ExecutionReadinessRequest; /// Aggregates Token-2022 postconditions without inventing success. -pub use self::solana_token_2022_execution_orchestration::summarize_token_2022_postconditions; +pub use self::spl_token_2022_execution_orchestration::summarize_token_2022_postconditions; /// Validates the complete Token-2022 execution envelope before signing. -pub use self::solana_token_2022_execution_orchestration::validate_token_2022_execution_readiness; +pub use self::spl_token_2022_execution_orchestration::validate_token_2022_execution_readiness; /// Migrated MAX_TOKEN_2022_PREFLIGHT_ACCOUNTS contract. -pub use self::solana_token_2022_preflight::MAX_TOKEN_2022_PREFLIGHT_ACCOUNTS; +pub use self::spl_token_2022_preflight::MAX_TOKEN_2022_PREFLIGHT_ACCOUNTS; /// Migrated MAX_TOKEN_2022_PREFLIGHT_TOTAL_BYTES contract. -pub use self::solana_token_2022_preflight::MAX_TOKEN_2022_PREFLIGHT_TOTAL_BYTES; +pub use self::spl_token_2022_preflight::MAX_TOKEN_2022_PREFLIGHT_TOTAL_BYTES; /// Migrated Token2022PreflightAccountReport contract. -pub use self::solana_token_2022_preflight::Token2022PreflightAccountReport; +pub use self::spl_token_2022_preflight::Token2022PreflightAccountReport; /// Migrated Token2022PreflightReport contract. -pub use self::solana_token_2022_preflight::Token2022PreflightReport; +pub use self::spl_token_2022_preflight::Token2022PreflightReport; /// Migrated Token2022PreflightRequest contract. -pub use self::solana_token_2022_preflight::Token2022PreflightRequest; +pub use self::spl_token_2022_preflight::Token2022PreflightRequest; /// Migrated Token2022PreflightRequirement contract. -pub use self::solana_token_2022_preflight::Token2022PreflightRequirement; +pub use self::spl_token_2022_preflight::Token2022PreflightRequirement; /// Migrated inspect_token_2022_preflight contract. -pub use self::solana_token_2022_preflight::inspect_token_2022_preflight; +pub use self::spl_token_2022_preflight::inspect_token_2022_preflight; /// Maximum number of proofs accepted for one Token-2022 operation. -pub use self::solana_token_2022_proof_orchestration::MAX_TOKEN_2022_OPERATION_PROOFS; +pub use self::spl_token_2022_proof_orchestration::MAX_TOKEN_2022_OPERATION_PROOFS; /// Token-2022 proof orchestration report. -pub use self::solana_token_2022_proof_orchestration::Token2022ProofOrchestrationReport; +pub use self::spl_token_2022_proof_orchestration::Token2022ProofOrchestrationReport; /// Token-2022 proof orchestration request. -pub use self::solana_token_2022_proof_orchestration::Token2022ProofOrchestrationRequest; +pub use self::spl_token_2022_proof_orchestration::Token2022ProofOrchestrationRequest; /// Validates mixed inline and context-state proof orchestration. -pub use self::solana_token_2022_proof_orchestration::orchestrate_token_2022_proofs; +pub use self::spl_token_2022_proof_orchestration::orchestrate_token_2022_proofs; /// Migrated MAX_TOKEN_2022_STATEFUL_ACCOUNT_BYTES contract. -pub use self::solana_token_2022_stateful::MAX_TOKEN_2022_STATEFUL_ACCOUNT_BYTES; +pub use self::spl_token_2022_stateful::MAX_TOKEN_2022_STATEFUL_ACCOUNT_BYTES; /// Migrated Token2022StatefulContext contract. -pub use self::solana_token_2022_stateful::Token2022StatefulContext; +pub use self::spl_token_2022_stateful::Token2022StatefulContext; /// Migrated Token2022StatefulReadRequest contract. -pub use self::solana_token_2022_stateful::Token2022StatefulReadRequest; +pub use self::spl_token_2022_stateful::Token2022StatefulReadRequest; /// Migrated Token2022StatefulReadResult contract. -pub use self::solana_token_2022_stateful::Token2022StatefulReadResult; +pub use self::spl_token_2022_stateful::Token2022StatefulReadResult; /// Migrated Token2022StatefulSnapshotBundle contract. -pub use self::solana_token_2022_stateful::Token2022StatefulSnapshotBundle; +pub use self::spl_token_2022_stateful::Token2022StatefulSnapshotBundle; /// Migrated materialize_parsed_token_2022_stateful_snapshot contract. -pub use self::solana_token_2022_stateful::materialize_parsed_token_2022_stateful_snapshot; +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::solana_token_2022_stateful::materialize_parsed_token_2022_stateful_snapshot_with_context; +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::solana_token_2022_stateful::materialize_token_2022_account_info_result; +pub use self::spl_token_2022_stateful::materialize_token_2022_account_info_result; /// Migrated materialize_token_2022_stateful_snapshot contract. -pub use self::solana_token_2022_stateful::materialize_token_2022_stateful_snapshot; +pub use self::spl_token_2022_stateful::materialize_token_2022_stateful_snapshot; /// Migrated read_token_2022_stateful_snapshot contract. -pub use self::solana_token_2022_stateful::read_token_2022_stateful_snapshot; +pub use self::spl_token_2022_stateful::read_token_2022_stateful_snapshot; /// One machine-readable classic SPL Token stateful check. -pub use self::solana_token_stateful::SplTokenStatefulCheck; +pub use self::spl_token_stateful::SplTokenStatefulCheck; /// One contextual classic SPL Token stateful fact. -pub use self::solana_token_stateful::SplTokenStatefulFact; +pub use self::spl_token_stateful::SplTokenStatefulFact; /// Complete classic SPL Token stateful readiness report. -pub use self::solana_token_stateful::SplTokenStatefulReadinessReport; +pub use self::spl_token_stateful::SplTokenStatefulReadinessReport; /// Complete request for one classic SPL Token stateful readiness inspection. -pub use self::solana_token_stateful::SplTokenStatefulReadinessRequest; +pub use self::spl_token_stateful::SplTokenStatefulReadinessRequest; /// Classic SPL Token stateful readiness outcome. -pub use self::solana_token_stateful::SplTokenStatefulReadinessStatus; +pub use self::spl_token_stateful::SplTokenStatefulReadinessStatus; /// Inspects Localnet or Devnet classic SPL Token state required before simulation. -pub use self::solana_token_stateful::inspect_spl_token_stateful_readiness; +pub use self::spl_token_stateful::inspect_spl_token_stateful_readiness; /// Canonical tracing target for pipeline orchestration. pub(crate) use self::constants::TRACING_TARGET; diff --git a/kb-pipeline/src/solana_metaplex_token_metadata_execution_orchestration.rs b/kb-pipeline/src/metadata_metaplex_token_metadata_execution_orchestration.rs similarity index 98% rename from kb-pipeline/src/solana_metaplex_token_metadata_execution_orchestration.rs rename to kb-pipeline/src/metadata_metaplex_token_metadata_execution_orchestration.rs index 35b3c4d..1854bbc 100644 --- a/kb-pipeline/src/solana_metaplex_token_metadata_execution_orchestration.rs +++ b/kb-pipeline/src/metadata_metaplex_token_metadata_execution_orchestration.rs @@ -1,5 +1,5 @@ -// file: kb-pipeline/src/solana_metaplex_token_metadata_execution_orchestration.rs -// version: 1 +// file: kb-pipeline/src/metadata_metaplex_token_metadata_execution_orchestration.rs +// version: 2 //! Simulation-first Metaplex Token Metadata execution orchestration. diff --git a/kb-pipeline/src/solana_metaplex_token_metadata_preflight.rs b/kb-pipeline/src/metadata_metaplex_token_metadata_preflight.rs similarity index 98% rename from kb-pipeline/src/solana_metaplex_token_metadata_preflight.rs rename to kb-pipeline/src/metadata_metaplex_token_metadata_preflight.rs index ca43045..b8d78bc 100644 --- a/kb-pipeline/src/solana_metaplex_token_metadata_preflight.rs +++ b/kb-pipeline/src/metadata_metaplex_token_metadata_preflight.rs @@ -1,5 +1,5 @@ -// file: kb-pipeline/src/solana_metaplex_token_metadata_preflight.rs -// version: 2 +// file: kb-pipeline/src/metadata_metaplex_token_metadata_preflight.rs +// version: 3 //! Stateful Metaplex Token Metadata preflight contracts. diff --git a/kb-pipeline/src/solana_metaplex_token_metadata_stateful.rs b/kb-pipeline/src/metadata_metaplex_token_metadata_stateful.rs similarity index 99% rename from kb-pipeline/src/solana_metaplex_token_metadata_stateful.rs rename to kb-pipeline/src/metadata_metaplex_token_metadata_stateful.rs index 9ff2702..e261c8b 100644 --- a/kb-pipeline/src/solana_metaplex_token_metadata_stateful.rs +++ b/kb-pipeline/src/metadata_metaplex_token_metadata_stateful.rs @@ -1,5 +1,5 @@ -// file: kb-pipeline/src/solana_metaplex_token_metadata_stateful.rs -// version: 3 +// file: kb-pipeline/src/metadata_metaplex_token_metadata_stateful.rs +// version: 4 //! Bounded Metaplex Token Metadata account reads and canonical state projections. diff --git a/kb-pipeline/src/metadata_solana_program_execution_orchestration.rs b/kb-pipeline/src/metadata_solana_program_execution_orchestration.rs new file mode 100644 index 0000000..6441987 --- /dev/null +++ b/kb-pipeline/src/metadata_solana_program_execution_orchestration.rs @@ -0,0 +1,831 @@ +// file: kb-pipeline/src/metadata_solana_program_execution_orchestration.rs +// version: 2 + +//! 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: kb_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, + /// 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, + /// Whether signing and submission are authorized. + pub send_authorized: bool, + /// Ordered successful checks. + pub checks: std::vec::Vec, +} + +/// 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: kb_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: kb_lib::ExMetadataSpmOperation, + /// Confirmed snapshots captured before execution. + pub before: std::vec::Vec, + /// Confirmed snapshots captured after execution. + pub after: std::vec::Vec, +} + +/// 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, +} + +/// Validates simulation, preflight, signer and submission policies. +pub fn validate_solana_program_metadata_execution_readiness( + request: &crate::SolanaProgramMetadataExecutionReadinessRequest, +) -> kb_core::Result { + if request.plan.operation_code != request.preflight.operation_code { + return std::result::Result::Err(kb_core::Error::new( + "solana_program_metadata_execution_operation_mismatch", + "Program Metadata plan and preflight operation codes must match", + )); + } + if request.preflight.safety.decision == kb_lib::ExSafetyDecision::Deny { + return std::result::Result::Err(kb_core::Error::new( + "solana_program_metadata_execution_safety_denied", + "Program Metadata preflight safety decision denied execution", + )); + } + if request.preflight.safety.decision == kb_lib::ExSafetyDecision::RequireConfirmation + && !request.operator_confirmed + { + return std::result::Result::Err(kb_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(kb_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(kb_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::>(); + let mut required = std::collections::BTreeMap::::new(); + for signer in &request.plan.required_signers { + if !resolved.contains(signer.pubkey.0.as_str()) { + return std::result::Result::Err(kb_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(kb_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(kb_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, +) -> kb_core::Result { + 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, +) -> kb_core::Result<()> { + if snapshots.len() > crate::MAX_SOLANA_PROGRAM_METADATA_PREFLIGHT_ACCOUNTS { + return std::result::Result::Err(kb_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(kb_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(kb_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: &kb_lib::ExMetadataSpmOperation) -> &kb_lib::MdPubkey { + return match operation { + kb_lib::ExMetadataSpmOperation::Write { buffer, .. } + | kb_lib::ExMetadataSpmOperation::Allocate { buffer, .. } => buffer, + kb_lib::ExMetadataSpmOperation::Initialize { metadata, .. } + | kb_lib::ExMetadataSpmOperation::SetData { metadata, .. } + | kb_lib::ExMetadataSpmOperation::SetImmutable { metadata, .. } => metadata, + kb_lib::ExMetadataSpmOperation::SetAuthority { account, .. } + | kb_lib::ExMetadataSpmOperation::Trim { account, .. } + | kb_lib::ExMetadataSpmOperation::Close { account, .. } + | kb_lib::ExMetadataSpmOperation::Extend { account, .. } => account, + }; +} + +fn find_snapshot<'a>( + snapshots: &'a [crate::SolanaProgramMetadataStatefulReadResult], + account: &kb_lib::MdPubkey, +) -> std::option::Option<&'a crate::SolanaProgramMetadataStatefulReadResult> { + return snapshots.iter().find(|value| return value.account == *account); +} + +fn evaluate_operation_postcondition( + operation: &kb_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 { + kb_lib::ExMetadataSpmOperation::Write { offset, source, .. } => { + write_postcondition(*offset, source, after, before_all) + }, + kb_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, + ), + kb_lib::ExMetadataSpmOperation::SetAuthority { new_authority, .. } => { + authority_postcondition(new_authority.as_ref(), after) + }, + kb_lib::ExMetadataSpmOperation::SetData { + encoding, compression, format, source, .. + } => set_data_postcondition( + *encoding, + *compression, + *format, + source, + before, + after, + before_all, + ), + kb_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, + }, + kb_lib::ExMetadataSpmOperation::Trim { .. } => trim_postcondition(before, after), + kb_lib::ExMetadataSpmOperation::Close { .. } => match after { + std::option::Option::Some(value) => { + matches!(&value.state, crate::SolanaProgramMetadataObservedAccountState::Missing) + }, + std::option::Option::None => false, + }, + kb_lib::ExMetadataSpmOperation::Allocate { authority, seed, canonical, .. } => { + allocate_postcondition(authority, seed.as_deref(), *canonical, after) + }, + kb_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: &kb_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 { + kb_lib::ExMetadataSpmWriteSource::Inline { data } => data.as_slice(), + kb_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: &kb_lib::MdPubkey, + canonical: bool, + seed: &[u8], + encoding: kb_lib::ExMetadataSpmEncoding, + compression: kb_lib::ExMetadataSpmCompression, + format: kb_lib::ExMetadataSpmFormat, + data_source: kb_lib::ExMetadataSpmDataSource, + data: std::option::Option<&kb_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<&kb_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: kb_lib::ExMetadataSpmEncoding, + compression: kb_lib::ExMetadataSpmCompression, + format: kb_lib::ExMetadataSpmFormat, + source: &kb_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 { + kb_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, + } + }, + kb_lib::ExMetadataSpmSetDataSource::Inline { data } => data_matches(data, &metadata.data), + kb_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 { + kb_lib::DcMetadataSpmData::Direct(value) => value.as_slice() == bytes, + kb_lib::DcMetadataSpmData::Url(value) => value.as_bytes() == bytes, + kb_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: &kb_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: &kb_lib::ExMetadataSpmDataInput, + observed: &kb_lib::DcMetadataSpmData, +) -> bool { + return match (input, observed) { + ( + kb_lib::ExMetadataSpmDataInput::Direct { bytes }, + kb_lib::DcMetadataSpmData::Direct(value), + ) => bytes == value, + (kb_lib::ExMetadataSpmDataInput::Url { url }, kb_lib::DcMetadataSpmData::Url(value)) => { + url == value + }, + ( + kb_lib::ExMetadataSpmDataInput::External { address, offset, length }, + kb_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) -> kb_lib::MdPubkey { + return kb_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 = kb_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: kb_lib::EX_METADATA_SPM_WRITE_OPERATION.to_string(), + fee_payer: signer.clone(), + instructions: vec![kb_lib::ExApiPlannedInstruction { + program_id: kb_lib::MdProgramId( + kb_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID.to_string(), + ), + operation_code: kb_lib::EX_METADATA_SPM_WRITE_OPERATION.to_string(), + accounts: vec![], + data: vec![0], + }], + required_signers: vec![kb_lib::ExApiRequiredSigner { + pubkey: signer.clone(), + role: "authority".to_string(), + }], + policy: kb_lib::ExApiExecutionPolicy { + dry_run: false, + ..kb_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: kb_lib::EX_METADATA_SPM_WRITE_OPERATION.to_string(), + target_account: pubkey(2), + context_slot: 99, + inspected_accounts: vec![], + safety: kb_lib::ExSafetyEvaluation { + decision: kb_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::(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"], + kb_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID + ); + assert_eq!( + matrix["operations"].as_array().map(std::vec::Vec::len), + std::option::Option::Some(kb_lib::EX_METADATA_SPM_SUPPORTED_OPERATION_CODES.len()) + ); + assert_eq!(matrix["pipelineCrate"], "kb-pipeline"); + assert_eq!(matrix["demoScenarioCrate"], "kb-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![ + kb_lib::ExMetadataSpmOperation::Write { + buffer: account.clone(), + authority: authority.clone(), + offset: 0, + source: kb_lib::ExMetadataSpmWriteSource::Inline { data: vec![1] }, + }, + kb_lib::ExMetadataSpmOperation::Initialize { + metadata: account.clone(), + authority: authority.clone(), + program: program.clone(), + program_data: std::option::Option::None, + canonical: false, + seed: vec![0_u8; kb_lib::DC_METADATA_SPM_SEED_BYTES], + encoding: kb_lib::ExMetadataSpmEncoding::Utf8, + compression: kb_lib::ExMetadataSpmCompression::None, + format: kb_lib::ExMetadataSpmFormat::Json, + data_source: kb_lib::ExMetadataSpmDataSource::Direct, + data: std::option::Option::Some(kb_lib::ExMetadataSpmDataInput::Direct { + bytes: vec![1], + }), + allocate_account: true, + }, + kb_lib::ExMetadataSpmOperation::SetAuthority { + account: account.clone(), + authority: authority.clone(), + new_authority: std::option::Option::Some(pubkey(8)), + program_context: std::option::Option::None, + }, + kb_lib::ExMetadataSpmOperation::SetData { + metadata: account.clone(), + authority: authority.clone(), + encoding: kb_lib::ExMetadataSpmEncoding::Utf8, + compression: kb_lib::ExMetadataSpmCompression::None, + format: kb_lib::ExMetadataSpmFormat::Json, + source: kb_lib::ExMetadataSpmSetDataSource::PreserveExisting, + program_context: std::option::Option::None, + }, + kb_lib::ExMetadataSpmOperation::SetImmutable { + metadata: account.clone(), + authority: authority.clone(), + program_context: std::option::Option::None, + }, + kb_lib::ExMetadataSpmOperation::Trim { + account: account.clone(), + authority: authority.clone(), + destination: destination.clone(), + program_context: std::option::Option::None, + }, + kb_lib::ExMetadataSpmOperation::Close { + account: account.clone(), + authority: authority.clone(), + destination, + program_context: std::option::Option::None, + }, + kb_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, + }, + kb_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::>(); + codes.sort(); + let mut expected = kb_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 + ); + } +} diff --git a/kb-pipeline/src/metadata_solana_program_preflight.rs b/kb-pipeline/src/metadata_solana_program_preflight.rs new file mode 100644 index 0000000..f8cfb00 --- /dev/null +++ b/kb-pipeline/src/metadata_solana_program_preflight.rs @@ -0,0 +1,761 @@ +// file: kb-pipeline/src/metadata_solana_program_preflight.rs +// version: 2 + +//! 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: kb_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: kb_lib::ExMetadataSpmExecutionIntent, + /// Exact prepared plan produced by `kb-lib`. + pub plan: kb_lib::ExApiPreparedExecutionPlan, + /// Confirmed account snapshots observed before simulation. + pub before: std::vec::Vec, + /// Explicit rent observations required by account growth operations. + pub rent_observations: std::vec::Vec, +} + +/// 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: kb_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, + /// Safety decision applied before simulation. + pub safety: kb_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, +} + +/// Validates one Program Metadata plan against confirmed pre-execution state. +pub fn inspect_solana_program_metadata_preflight( + request: &crate::SolanaProgramMetadataPreflightRequest, +) -> kb_core::Result { + if request.before.len() > crate::MAX_SOLANA_PROGRAM_METADATA_PREFLIGHT_ACCOUNTS { + return std::result::Result::Err(kb_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(kb_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(kb_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(kb_core::Error::new( + "metadata_solana_program_preflight_instruction_missing", + "Program Metadata plan instruction is missing", + )); + }, + }; + if instruction.program_id.0 != kb_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID + || instruction.operation_code != operation_code + || !kb_lib::EX_METADATA_SPM_SUPPORTED_OPERATION_CODES.contains(&operation_code) + { + return std::result::Result::Err(kb_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(kb_core::Error::new( + "metadata_solana_program_preflight_destructive_approval_required", + "destructive Program Metadata execution requires explicit approval", + )); + } + let safety = match kb_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 == kb_lib::ExSafetyDecision::Deny { + return std::result::Result::Err(kb_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(kb_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(kb_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(kb_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::>(); + 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: &kb_lib::ExMetadataSpmOperation) -> &kb_lib::MdPubkey { + return match operation { + kb_lib::ExMetadataSpmOperation::Write { buffer, .. } + | kb_lib::ExMetadataSpmOperation::Allocate { buffer, .. } => buffer, + kb_lib::ExMetadataSpmOperation::Initialize { metadata, .. } + | kb_lib::ExMetadataSpmOperation::SetData { metadata, .. } + | kb_lib::ExMetadataSpmOperation::SetImmutable { metadata, .. } => metadata, + kb_lib::ExMetadataSpmOperation::SetAuthority { account, .. } + | kb_lib::ExMetadataSpmOperation::Trim { account, .. } + | kb_lib::ExMetadataSpmOperation::Close { account, .. } + | kb_lib::ExMetadataSpmOperation::Extend { account, .. } => account, + }; +} + +fn validate_target_state( + operation: &kb_lib::ExMetadataSpmOperation, + target: &crate::SolanaProgramMetadataStatefulReadResult, +) -> kb_core::Result<()> { + let valid = match operation { + kb_lib::ExMetadataSpmOperation::Write { .. } => { + matches!(&target.state, crate::SolanaProgramMetadataObservedAccountState::Buffer(_)) + }, + kb_lib::ExMetadataSpmOperation::Initialize { data, .. } => { + if data.is_some() { + is_prefunded_uninitialized(target) + } else { + matches!(&target.state, crate::SolanaProgramMetadataObservedAccountState::Buffer(_)) + } + }, + kb_lib::ExMetadataSpmOperation::SetData { .. } + | kb_lib::ExMetadataSpmOperation::SetImmutable { .. } => { + matches!(&target.state, crate::SolanaProgramMetadataObservedAccountState::Metadata(_)) + }, + kb_lib::ExMetadataSpmOperation::Allocate { .. } => is_prefunded_uninitialized(target), + kb_lib::ExMetadataSpmOperation::SetAuthority { .. } + | kb_lib::ExMetadataSpmOperation::Trim { .. } + | kb_lib::ExMetadataSpmOperation::Close { .. } + | kb_lib::ExMetadataSpmOperation::Extend { .. } => matches!( + &target.state, + crate::SolanaProgramMetadataObservedAccountState::Buffer(_) + | crate::SolanaProgramMetadataObservedAccountState::Metadata(_) + ), + }; + if !valid { + return std::result::Result::Err(kb_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 == kb_program_ids::SYSTEM_PROGRAM_ID + || owner.0 == kb_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID) + }, + _ => false, + }; +} + +fn validate_source_state( + operation: &kb_lib::ExMetadataSpmOperation, + snapshots: &std::collections::BTreeMap< + std::string::String, + &crate::SolanaProgramMetadataStatefulReadResult, + >, + target: &crate::SolanaProgramMetadataStatefulReadResult, +) -> kb_core::Result<()> { + match operation { + kb_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 { + kb_lib::ExMetadataSpmWriteSource::Inline { data } => data.len(), + kb_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(kb_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(kb_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(kb_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(kb_core::Error::new( + "metadata_solana_program_preflight_write_out_of_bounds", + "Write source exceeds the allocated Buffer capacity", + )); + } + }, + kb_lib::ExMetadataSpmOperation::SetData { + source: kb_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(kb_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(kb_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: &kb_lib::ExMetadataSpmOperation, + target: &crate::SolanaProgramMetadataStatefulReadResult, +) -> kb_core::Result { + let (authority, has_program_context) = match operation { + kb_lib::ExMetadataSpmOperation::Write { authority: _, .. } => { + return std::result::Result::Ok(false); + }, + kb_lib::ExMetadataSpmOperation::Initialize { canonical, .. } => { + return std::result::Result::Ok(*canonical); + }, + kb_lib::ExMetadataSpmOperation::Allocate { seed, canonical, .. } => { + return std::result::Result::Ok(seed.is_some() && *canonical); + }, + kb_lib::ExMetadataSpmOperation::SetAuthority { authority, program_context, .. } + | kb_lib::ExMetadataSpmOperation::SetData { authority, program_context, .. } + | kb_lib::ExMetadataSpmOperation::SetImmutable { authority, program_context, .. } + | kb_lib::ExMetadataSpmOperation::Trim { authority, program_context, .. } + | kb_lib::ExMetadataSpmOperation::Close { authority, program_context, .. } + | kb_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(kb_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: &kb_lib::ExMetadataSpmOperation, + target: &crate::SolanaProgramMetadataStatefulReadResult, + snapshots: &std::collections::BTreeMap< + std::string::String, + &crate::SolanaProgramMetadataStatefulReadResult, + >, + observations: &[crate::SolanaProgramMetadataRentObservation], +) -> kb_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(kb_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(kb_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: &kb_lib::ExMetadataSpmOperation, + target: &crate::SolanaProgramMetadataStatefulReadResult, + snapshots: &std::collections::BTreeMap< + std::string::String, + &crate::SolanaProgramMetadataStatefulReadResult, + >, +) -> kb_core::Result> { + let header = match u64::try_from(kb_lib::DC_METADATA_SPM_METADATA_HEADER_BYTES) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return std::result::Result::Err(kb_core::Error::new( + "metadata_solana_program_preflight_header_size_overflow", + error.to_string(), + )); + }, + }; + return match operation { + kb_lib::ExMetadataSpmOperation::Initialize { + data: std::option::Option::Some(data), + .. + } => add_metadata_header(header, metadata_input_wire_len(data)), + kb_lib::ExMetadataSpmOperation::SetData { + source: kb_lib::ExMetadataSpmSetDataSource::PreserveExisting, + .. + } => std::result::Result::Ok(std::option::Option::None), + kb_lib::ExMetadataSpmOperation::SetData { + source: kb_lib::ExMetadataSpmSetDataSource::Inline { data }, + .. + } => add_metadata_header(header, metadata_input_wire_len(data)), + kb_lib::ExMetadataSpmOperation::SetData { + source: kb_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(kb_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(kb_core::Error::new( + "metadata_solana_program_preflight_source_length_overflow", + error.to_string(), + )); + }, + } + }, + _ => { + return std::result::Result::Err(kb_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)); + }, + kb_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))) + }, + kb_lib::ExMetadataSpmOperation::Extend { length, .. } => { + let current = match target.space { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(kb_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(kb_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: &kb_lib::ExMetadataSpmDataInput) -> kb_core::Result { + let length = match data { + kb_lib::ExMetadataSpmDataInput::Direct { bytes } => bytes.len(), + kb_lib::ExMetadataSpmDataInput::Url { url } => url.len(), + kb_lib::ExMetadataSpmDataInput::External { .. } => { + kb_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(kb_core::Error::new( + "metadata_solana_program_preflight_data_length_overflow", + error.to_string(), + )), + }; +} + +fn add_metadata_header( + header: u64, + data_length: kb_core::Result, +) -> kb_core::Result> { + 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(kb_core::Error::new( + "metadata_solana_program_preflight_target_space_overflow", + "Program Metadata target space overflowed u64", + )), + }; +} + +fn safety_violation_codes(violations: &[kb_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::>() + .join(","); +} + +#[cfg(test)] +mod tests { + fn pubkey(value: u8) -> kb_lib::MdPubkey { + return kb_lib::MdPubkey(solana_pubkey::Pubkey::new_from_array([value; 32]).to_string()); + } + + fn policy(authority: &kb_lib::MdPubkey) -> kb_lib::ExApiExecutionPolicy { + return kb_lib::ExApiExecutionPolicy { + cost_limit: kb_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: kb_lib::ExApiPostExecutionValidationPolicy { + canonical_insert_required: true, + core_extraction_required: true, + decode_replay_required: true, + materialization_required: true, + }, + ..kb_lib::ExApiExecutionPolicy::default() + }; + } + + fn buffer_read( + account: kb_lib::MdPubkey, + authority: kb_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(kb_lib::MdProgramId( + kb_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID.to_string(), + )), + space: std::option::Option::Some(100), + state: crate::SolanaProgramMetadataObservedAccountState::Buffer( + kb_lib::DcMetadataSpmBufferAccountSnapshot { + account: account.0, + program: std::option::Option::None, + authority: std::option::Option::Some(authority.0), + canonical: false, + seed: kb_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 = kb_lib::ExMetadataSpmExecutionIntent { + intent_id: "spm-preflight-write".to_string(), + fee_payer: authority.clone(), + policy: policy(&authority), + allow_destructive_operation: true, + operation: kb_lib::ExMetadataSpmOperation::Write { + buffer: buffer.clone(), + authority: authority.clone(), + offset: 2, + source: kb_lib::ExMetadataSpmWriteSource::Inline { data: vec![8, 9] }, + }, + }; + let plan = kb_lib::ExApiTypedInstructionExecutor::build_prepared_plan( + &kb_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, kb_lib::EX_METADATA_SPM_WRITE_OPERATION); + assert_eq!(value.safety.decision, kb_lib::ExSafetyDecision::Allow); + } + } + + #[test] + fn allocate_requires_vacancy_and_explicit_rent_evidence() { + let authority = pubkey(4); + let buffer = authority.clone(); + let intent = kb_lib::ExMetadataSpmExecutionIntent { + intent_id: "spm-preflight-allocate".to_string(), + fee_payer: authority.clone(), + policy: policy(&authority), + allow_destructive_operation: false, + operation: kb_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 = kb_lib::ExApiTypedInstructionExecutor::build_prepared_plan( + &kb_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(kb_lib::MdProgramId( + kb_program_ids::SYSTEM_PROGRAM_ID.to_string(), + )), + space: std::option::Option::Some(0), + state: crate::SolanaProgramMetadataObservedAccountState::Uninitialized { + owner: kb_lib::MdProgramId(kb_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(kb_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()); + } +} diff --git a/kb-pipeline/src/metadata_solana_program_stateful.rs b/kb-pipeline/src/metadata_solana_program_stateful.rs new file mode 100644 index 0000000..f12a3a2 --- /dev/null +++ b/kb-pipeline/src/metadata_solana_program_stateful.rs @@ -0,0 +1,425 @@ +// file: kb-pipeline/src/metadata_solana_program_stateful.rs +// version: 2 + +//! Bounded Solana Program Metadata account reads and authoritative state projections. + +/// Maximum complete Solana Program Metadata account data accepted by one RPC read. +pub const MAX_SOLANA_PROGRAM_METADATA_STATEFUL_ACCOUNT_BYTES: usize = + kb_lib::DC_METADATA_SPM_MAX_ACCOUNT_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: kb_lib::MdProgramId, + /// Current lamports retained by the account. + lamports: u64, + /// Current allocated account-data bytes. + space: u64, + }, + /// Decoded Program Metadata Buffer account. + Buffer(kb_lib::DcMetadataSpmBufferAccountSnapshot), + /// Decoded Program Metadata Metadata account. + Metadata(kb_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: kb_lib::MdPubkey, + /// Expected account state. + pub expected_state: crate::SolanaProgramMetadataExpectedAccountState, + /// Optional minimum RPC context slot. + pub min_context_slot: std::option::Option, + /// Maximum decoded account bytes accepted from the endpoint. + pub max_data_bytes: usize, +} + +impl crate::SolanaProgramMetadataStatefulReadRequest { + /// Validates the bounded request. + pub fn validate(&self) -> kb_core::Result<()> { + if self.query_role.trim().is_empty() { + return std::result::Result::Err(kb_core::Error::config( + "Solana Program Metadata stateful query_role must not be empty", + )); + } + let account_result = kb_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(kb_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: kb_lib::MdPubkey, + /// Current lamports when the account exists. + pub lamports: std::option::Option, + /// Current owner when the account exists. + pub owner: std::option::Option, + /// Current account-data allocation when the account exists. + pub space: std::option::Option, + /// Validated account state. + pub state: crate::SolanaProgramMetadataObservedAccountState, + /// Authoritative materialized projection for Buffer or Metadata state. + pub materialized_output: std::option::Option, +} + +/// Reads one bounded Solana Program Metadata account. +pub async fn read_solana_program_metadata_stateful_snapshot( + pool: &kb_onchain_transport::HttpEndpointPool, + request: &crate::SolanaProgramMetadataStatefulReadRequest, +) -> kb_core::Result { + if let std::result::Result::Err(error) = request.validate() { + return std::result::Result::Err(error); + } + let config = match kb_onchain_transport::GetAccountInfoConfig::new_with_data( + kb_onchain_transport::RpcCommitmentLevel::Confirmed, + request.min_context_slot, + request.max_data_bytes, + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let result = match pool + .get_account_info_for_role(request.query_role.as_str(), &request.account, &config) + .await + { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return crate::materialize_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: &kb_onchain_transport::AccountInfoResult, +) -> kb_core::Result { + 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(kb_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(kb_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(kb_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(kb_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 != kb_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID { + return std::result::Result::Err(kb_core::Error::new( + "metadata_solana_program_stateful_owner_mismatch", + "initialized Solana Program Metadata state must be owned by ProgM6", + )); + } + let snapshot = match kb_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(kb_core::Error::new( + "metadata_solana_program_stateful_decode_failed", + error.to_string(), + )); + }, + }; + let materialized = match kb_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(kb_core::Error::new( + "metadata_solana_program_stateful_materialization_failed", + error, + )); + }, + }; + let state = match snapshot { + kb_lib::DcMetadataSpmAccountSnapshot::Buffer(value) => { + crate::SolanaProgramMetadataObservedAccountState::Buffer(value) + }, + kb_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(kb_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: kb_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 = kb_onchain_transport::AccountInfoResult { + context: kb_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 = kb_onchain_transport::AccountInfoResult { + context: kb_onchain_transport::RpcResponseContext { + slot: 11, + api_version: std::option::Option::None, + }, + account: std::option::Option::Some(kb_onchain_transport::AccountInfoValue { + lamports: 100, + owner: kb_lib::MdProgramId(kb_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; kb_lib::DC_METADATA_SPM_BUFFER_HEADER_BYTES]; + data[0] = kb_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 = kb_onchain_transport::AccountInfoResult { + context: kb_onchain_transport::RpcResponseContext { + slot: 12, + api_version: std::option::Option::None, + }, + account: std::option::Option::Some(kb_onchain_transport::AccountInfoValue { + lamports: 1_000, + owner: kb_lib::MdProgramId( + kb_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")) + ); + } + } +} diff --git a/kb-pipeline/src/solana_ata_stateful.rs b/kb-pipeline/src/spl_ata_stateful.rs similarity index 99% rename from kb-pipeline/src/solana_ata_stateful.rs rename to kb-pipeline/src/spl_ata_stateful.rs index 428804d..99c6c69 100644 --- a/kb-pipeline/src/solana_ata_stateful.rs +++ b/kb-pipeline/src/spl_ata_stateful.rs @@ -1,5 +1,5 @@ -// file: kb-pipeline/src/solana_ata_stateful.rs -// version: 3 +// file: kb-pipeline/src/spl_ata_stateful.rs +// version: 4 //! Stateful Localnet and Devnet readiness checks for ATA operations. diff --git a/kb-pipeline/src/solana_elgamal_registry_stateful.rs b/kb-pipeline/src/spl_elgamal_registry_stateful.rs similarity index 99% rename from kb-pipeline/src/solana_elgamal_registry_stateful.rs rename to kb-pipeline/src/spl_elgamal_registry_stateful.rs index 9915ad5..0f2d1f6 100644 --- a/kb-pipeline/src/solana_elgamal_registry_stateful.rs +++ b/kb-pipeline/src/spl_elgamal_registry_stateful.rs @@ -1,5 +1,5 @@ -// file: kb-pipeline/src/solana_elgamal_registry_stateful.rs -// version: 4 +// file: kb-pipeline/src/spl_elgamal_registry_stateful.rs +// version: 5 //! Contextual SPL ElGamal registry account-state validation and materialization routing. diff --git a/kb-pipeline/src/solana_token_2022_correlation.rs b/kb-pipeline/src/spl_token_2022_correlation.rs similarity index 99% rename from kb-pipeline/src/solana_token_2022_correlation.rs rename to kb-pipeline/src/spl_token_2022_correlation.rs index 114ed75..eb0bef0 100644 --- a/kb-pipeline/src/solana_token_2022_correlation.rs +++ b/kb-pipeline/src/spl_token_2022_correlation.rs @@ -1,5 +1,5 @@ -// file: kb-pipeline/src/solana_token_2022_correlation.rs -// version: 2 +// file: kb-pipeline/src/spl_token_2022_correlation.rs +// version: 3 //! Deterministic Token-2022 instruction-to-state correlation. diff --git a/kb-pipeline/src/solana_token_2022_crypto_preflight.rs b/kb-pipeline/src/spl_token_2022_crypto_preflight.rs similarity index 99% rename from kb-pipeline/src/solana_token_2022_crypto_preflight.rs rename to kb-pipeline/src/spl_token_2022_crypto_preflight.rs index 02bbbd2..4cdaa75 100644 --- a/kb-pipeline/src/solana_token_2022_crypto_preflight.rs +++ b/kb-pipeline/src/spl_token_2022_crypto_preflight.rs @@ -1,5 +1,5 @@ -// file: kb-pipeline/src/solana_token_2022_crypto_preflight.rs -// version: 3 +// file: kb-pipeline/src/spl_token_2022_crypto_preflight.rs +// version: 4 //! Bounded cryptographic preflight for Token-2022 proof context-state accounts. diff --git a/kb-pipeline/src/solana_token_2022_execution_orchestration.rs b/kb-pipeline/src/spl_token_2022_execution_orchestration.rs similarity index 99% rename from kb-pipeline/src/solana_token_2022_execution_orchestration.rs rename to kb-pipeline/src/spl_token_2022_execution_orchestration.rs index a273ba8..5c44ee8 100644 --- a/kb-pipeline/src/solana_token_2022_execution_orchestration.rs +++ b/kb-pipeline/src/spl_token_2022_execution_orchestration.rs @@ -1,5 +1,5 @@ -// file: kb-pipeline/src/solana_token_2022_execution_orchestration.rs -// version: 3 +// file: kb-pipeline/src/spl_token_2022_execution_orchestration.rs +// version: 4 //! Final Token-2022 execution-readiness and stateful postcondition contracts. diff --git a/kb-pipeline/src/solana_token_2022_preflight.rs b/kb-pipeline/src/spl_token_2022_preflight.rs similarity index 99% rename from kb-pipeline/src/solana_token_2022_preflight.rs rename to kb-pipeline/src/spl_token_2022_preflight.rs index 9af6743..2c6fd21 100644 --- a/kb-pipeline/src/solana_token_2022_preflight.rs +++ b/kb-pipeline/src/spl_token_2022_preflight.rs @@ -1,5 +1,5 @@ -// file: kb-pipeline/src/solana_token_2022_preflight.rs -// version: 3 +// file: kb-pipeline/src/spl_token_2022_preflight.rs +// version: 4 //! Bounded Token-2022 stateful preflight orchestration. diff --git a/kb-pipeline/src/solana_token_2022_proof_orchestration.rs b/kb-pipeline/src/spl_token_2022_proof_orchestration.rs similarity index 99% rename from kb-pipeline/src/solana_token_2022_proof_orchestration.rs rename to kb-pipeline/src/spl_token_2022_proof_orchestration.rs index eee4be7..8aac76c 100644 --- a/kb-pipeline/src/solana_token_2022_proof_orchestration.rs +++ b/kb-pipeline/src/spl_token_2022_proof_orchestration.rs @@ -1,5 +1,5 @@ -// file: kb-pipeline/src/solana_token_2022_proof_orchestration.rs -// version: 4 +// file: kb-pipeline/src/spl_token_2022_proof_orchestration.rs +// version: 5 //! Deterministic orchestration contract for mixed Token-2022 proof locations. diff --git a/kb-pipeline/src/solana_token_2022_stateful.rs b/kb-pipeline/src/spl_token_2022_stateful.rs similarity index 99% rename from kb-pipeline/src/solana_token_2022_stateful.rs rename to kb-pipeline/src/spl_token_2022_stateful.rs index db0de59..805f7ad 100644 --- a/kb-pipeline/src/solana_token_2022_stateful.rs +++ b/kb-pipeline/src/spl_token_2022_stateful.rs @@ -1,5 +1,5 @@ -// file: kb-pipeline/src/solana_token_2022_stateful.rs -// version: 7 +// file: kb-pipeline/src/spl_token_2022_stateful.rs +// version: 8 //! Contextual Token-2022 account-state validation and materialization routing. diff --git a/kb-pipeline/src/solana_token_stateful.rs b/kb-pipeline/src/spl_token_stateful.rs similarity index 99% rename from kb-pipeline/src/solana_token_stateful.rs rename to kb-pipeline/src/spl_token_stateful.rs index c7bf674..f1f06d7 100644 --- a/kb-pipeline/src/solana_token_stateful.rs +++ b/kb-pipeline/src/spl_token_stateful.rs @@ -1,5 +1,5 @@ -// file: kb-pipeline/src/solana_token_stateful.rs -// version: 1 +// file: kb-pipeline/src/spl_token_stateful.rs +// version: 2 //! Stateful Localnet and Devnet readiness checks for classic SPL Token operations. diff --git a/kb-pipeline/tests/external_metadata_solana_program_pipeline_api.rs b/kb-pipeline/tests/external_metadata_solana_program_pipeline_api.rs new file mode 100644 index 0000000..80fa679 --- /dev/null +++ b/kb-pipeline/tests/external_metadata_solana_program_pipeline_api.rs @@ -0,0 +1,31 @@ +// file: kb-pipeline/tests/external_metadata_solana_program_pipeline_api.rs +// version: 2 + +//! External-only coverage for the public Solana Program Metadata pipeline contract. + +#[test] +fn external_consumers_use_only_public_program_metadata_pipeline_exports() { + let account = kb_lib::MdPubkey(solana_pubkey::Pubkey::new_from_array([7_u8; 32]).to_string()); + let request = kb_pipeline::SolanaProgramMetadataStatefulReadRequest { + query_role: "rpc".to_string(), + account: account.clone(), + expected_state: kb_pipeline::SolanaProgramMetadataExpectedAccountState::Any, + min_context_slot: std::option::Option::None, + max_data_bytes: kb_pipeline::MAX_SOLANA_PROGRAM_METADATA_STATEFUL_ACCOUNT_BYTES, + }; + assert!(request.validate().is_ok()); + let postcondition = kb_pipeline::SolanaProgramMetadataPostcondition { + code: "metadata.solana_program_metadata.external_api".to_string(), + account, + status: kb_pipeline::SolanaProgramMetadataPostconditionStatus::NotApplicable, + diagnostic: "external API coverage".to_string(), + }; + assert_eq!( + kb_pipeline::summarize_solana_program_metadata_postconditions(&[postcondition]), + kb_pipeline::SolanaProgramMetadataPostconditionStatus::NotApplicable + ); + const { + assert!(kb_pipeline::MAX_SOLANA_PROGRAM_METADATA_EXECUTION_SIGNERS > 0); + assert!(kb_pipeline::MAX_SOLANA_PROGRAM_METADATA_PREFLIGHT_ACCOUNTS > 0); + } +} diff --git a/test-fixtures/contract-matrices/METADATA_SOLANA_PROGRAM_PIPELINE_MATRIX.json b/test-fixtures/contract-matrices/METADATA_SOLANA_PROGRAM_PIPELINE_MATRIX.json new file mode 100644 index 0000000..db7ba6c --- /dev/null +++ b/test-fixtures/contract-matrices/METADATA_SOLANA_PROGRAM_PIPELINE_MATRIX.json @@ -0,0 +1,77 @@ +{ + "schemaVersion": 1, + "programId": "ProgM6JCCvbYkfKqJYHePx4xxSUSqJp7rh8Lyv7nk7S", + "pipelineCrate": "kb-pipeline", + "demoScenarioCrate": "kb-pipeline-demo-scenarios", + "historicalValidationFlow": [ + "demo_backfill", + "core_extraction", + "demo_decode_replay", + "materialized_event_query" + ], + "operations": [ + { + "code": "metadata.solana_program_metadata.write", + "before": ["buffer", "optional_source_buffer"], + "rentObservationPolicy": "none", + "explicitApprovalRequired": true, + "after": "buffer_bytes_match_source_at_offset" + }, + { + "code": "metadata.solana_program_metadata.initialize", + "before": ["vacant_or_buffer"], + "rentObservationPolicy": "exact_target_space", + "explicitApprovalRequired": false, + "after": "metadata_header_and_content_match_intent" + }, + { + "code": "metadata.solana_program_metadata.set_authority", + "before": ["buffer_or_metadata"], + "rentObservationPolicy": "none", + "explicitApprovalRequired": true, + "after": "authority_matches_intent" + }, + { + "code": "metadata.solana_program_metadata.set_data", + "before": ["metadata", "optional_source_buffer"], + "rentObservationPolicy": "exact_target_space", + "explicitApprovalRequired": true, + "after": "metadata_descriptors_and_content_match_intent" + }, + { + "code": "metadata.solana_program_metadata.set_immutable", + "before": ["metadata"], + "rentObservationPolicy": "none", + "explicitApprovalRequired": true, + "after": "metadata_is_immutable" + }, + { + "code": "metadata.solana_program_metadata.trim", + "before": ["buffer_or_metadata"], + "rentObservationPolicy": "none", + "explicitApprovalRequired": true, + "after": "same_state_kind_with_non_increasing_space" + }, + { + "code": "metadata.solana_program_metadata.close", + "before": ["buffer_or_metadata"], + "rentObservationPolicy": "none", + "explicitApprovalRequired": true, + "after": "account_is_missing" + }, + { + "code": "metadata.solana_program_metadata.allocate", + "before": ["vacant"], + "rentObservationPolicy": "exact_target_space", + "explicitApprovalRequired": false, + "after": "buffer_header_matches_authority_seed_and_canonicity" + }, + { + "code": "metadata.solana_program_metadata.extend", + "before": ["buffer_or_metadata"], + "rentObservationPolicy": "exact_target_space", + "explicitApprovalRequired": false, + "after": "same_state_kind_with_exact_extended_space" + } + ] +}