v0.1.0-pre.010

This commit is contained in:
2026-07-23 21:41:09 +02:00
parent 51ed3d11cf
commit 62bc0ffb44
25 changed files with 3863 additions and 214 deletions

View File

@@ -1,5 +1,5 @@
<!-- file: README.md -->
<!-- version: 6 -->
<!-- version: 7 -->
# Khadhroony Bot3
@@ -58,3 +58,14 @@ cargo clippy --workspace --all-targets
`0.1.0-pre.008` porte le décodeur Token2022 maximal, son parseur détats Mint/Account/Multisig et de TLV, ainsi que le programme indépendant du registre ElGamal. Les deux composants gardent des frontières et des targets de tracing distincts. Tous les décodeurs migrés utilisent les mêmes contrats contextualisés, la même façade publique et aucune dépendance inverse vers `kb-store`.
`0.1.0-pre.009` porte le décodeur indépendant Metaplex Token Metadata. Il couvre les 58 discriminateurs dinstructions publiés `0..=57`, les 15 variantes de comptes `Key 0..=14`, les PDA officiels et les layouts courants, expérimentaux et historiques. Les metadata externes Metaplex restent séparées des metadata incorporées de Token2022, de Metaplex Core et de Bubblegum.
## Matérialisateurs consolidés
`0.1.0-pre.010` porte les quatre matérialisateurs Solana natifs dans `kb-lib` :
- `LifecycleMaterializer` pour les comptes System, nonces, ALT, loaders, Feature Gate, contextes ZK et rapports Slashing ;
- `AdminMaterializer` pour les assignations, configurations et autorités, avec ses projections stateful Token2022 et registre ElGamal ;
- `ComplianceAuditMaterializer` pour les écritures et copies bornées de bytecode ;
- `StakingMaterializer` pour les transitions Stake et Vote commitées.
Les identités historiques des processors sont conservées pour la compatibilité des replays, tandis que les targets de tracing suivent désormais la hiérarchie `kb-lib.materializer.*`. Le contrat public `EventMaterializer`, ses modèles dentrée et ses résultats restent réexportés à la racine de `kb-lib`, afin quun futur crate externe puisse fournir un matérialiseur sans dépendre dune implémentation interne.

View File

@@ -1,5 +1,5 @@
<!-- file: ROADMAP.md -->
<!-- version: 7 -->
<!-- version: 8 -->
# ROADMAP — khadhroony-bot3
@@ -14,10 +14,12 @@
- [x] Porter exactement les contrats publics des APIs dexécution et de matérialisation.
- [x] Porter les modèles de `kb_model` vers `kb-lib`.
- [~] Porter les décodeurs Solana core, SPL et Metaplex.
- [ ] Porter leurs matérialisateurs et exécuteurs.
- [~] Porter leurs matérialisateurs.
- [ ] Porter leurs exécuteurs.
- [x] Fusionner `kb_store_core` et `kb_store_pg` dans `kb-store`.
- [ ] Adapter `kb-pipeline` aux nouveaux chemins publics.
- [ ] Adapter `kb-app-demo` et rétablir les validations fonctionnelles.
- [ ] Créer ultérieurement une crate off-chain dédiée au fetch borné HTTP/IPFS/Arweave des URI de metadata, séparée des décodeurs, matérialisateurs et du replay canonique on-chain.
- [ ] Réorganiser les chemins dexport TSRS de `kb-lib` dans une tranche dédiée, en conservant `../frontend/ts/bindings/` comme préfixe de base.
- [ ] Auditer rétrospectivement la couverture historique des décodeurs et matérialisateurs.
@@ -110,5 +112,17 @@
- [x] Ajouter le Program ID Metaplex Token Metadata à `kb-program-ids` sans le classer comme surface native.
- [x] Porter le target de tracing vers `kb-lib.decoder.metadata.metaplex_token_metadata`.
- [x] Conserver les 69 tests Metaplex et la matrice machine-readable exhaustive.
- [ ] Valider `kb-program-ids`, les 320 tests de `kb-lib`, Clippy et le workspace avec Cargo sur la machine de développement.
- [ ] Poursuivre avec les matérialisateurs et exécuteurs après validation du port des décodeurs prioritaires.
- [x] Valider `kb-program-ids`, les 320 tests de `kb-lib`, Clippy et le workspace avec Cargo sur la machine de développement.
- [x] Poursuivre avec les matérialisateurs après validation du port des décodeurs prioritaires.
### 0.1.0-pre.010 — Matérialisateurs Solana natifs dans `kb-lib`
- [x] Porter `LifecycleMaterializer`, `AdminMaterializer`, `ComplianceAuditMaterializer` et `StakingMaterializer` dans les modules consolidés de `kb-lib`.
- [x] Conserver leurs 50 tests de projections natives, de politique de transaction et didempotence.
- [x] Réexporter les types concrets et les fonctions stateful publiques depuis la façade de `kb-lib`.
- [x] Ajouter un test dintégration aval prouvant quun futur crate externe peut implémenter `EventMaterializer` avec la seule API publique.
- [x] Porter les targets vers `kb-lib.materializer.admin`, `kb-lib.materializer.compliance`, `kb-lib.materializer.lifecycle` et `kb-lib.materializer.staking`.
- [x] Conserver les identités de processor historiques afin de préserver les clés de replay et didempotence existantes.
- [x] Ajouter la règle dordre et dabsence de ligne vide interne dans les blocs de réexports publics.
- [ ] Valider les 371 tests attendus de `kb-lib`, Clippy et le workspace avec Cargo sur la machine de développement.
- [ ] Poursuivre avec les matérialisateurs Memo, SPL Token, ATA, Token2022, registre ElGamal et Metaplex.

View File

@@ -1,5 +1,5 @@
<!-- file: RULES.md -->
<!-- version: 18 -->
<!-- version: 19 -->
# Règles spécifiques à `khadhroony-bot3`
@@ -17,6 +17,7 @@ Toute livraison doit exécuter l'audit `python3 scripts/audit_rust_workspace_rul
- Les packages Rust utilisent le préfixe `kb-` ; leur identifiant Rust correspondant utilise automatiquement `kb_`.
- Les décodeurs, matérialisateurs et exécuteurs sont des modules de `kb-lib`, pas des crates séparées.
- Le crate de journalisation s'appelle `kb-logging`.
- Les blocs consécutifs de réexports `pub use` et `pub(crate) use` ne contiennent aucune ligne vide interne et restent ordonnés alphabétiquement lorsque `cargo fmt` ne les réordonne pas.
## Règles Tauri et TypeScript

View File

@@ -1,5 +1,5 @@
<!-- file: config/README.md -->
<!-- version: 13 -->
<!-- version: 14 -->
# Configuration locale
@@ -50,13 +50,13 @@ Les sources futures devront déclarer séparément :
Lexemple configure, pour chacun des trois profils, quatre fichiers globaux puis trois fichiers dédiés par crate opérationnelle utilisant `tracing` : `debug.log`, `info.log` et `error.jsonl`.
`pre.020` rend `kb_materializer_admin` et `kb_materializer_compliance_audit` opérationnelles. `pre.021` active ensuite `kb_materializer_staking`. Elles rejoignent donc la console et disposent de leurs routes propres sous :
Les matérialisateurs natifs consolidés rejoignent la console et disposent de leurs routes propres sous :
```text
logs/<profil>/kb_materializer_admin/
logs/<profil>/kb_materializer_compliance_audit/
logs/<profil>/kb_materializer_staking/
logs/<profil>/kb-lib/materializer/admin/
logs/<profil>/kb-lib/materializer/compliance/
logs/<profil>/kb-lib/materializer/lifecycle/
logs/<profil>/kb-lib/materializer/staking/
```
Le test de configuration découvre dynamiquement toutes les crates déclarant `tracing.workspace = true` et vérifie leurs trois routes dans chaque profil. Les logs ne doivent contenir ni secret, ni DSN non masqué, ni payload Config ou bytecode complet.

View File

@@ -42,11 +42,11 @@
"kb_executor_spl_token",
"kb_executor_spl_token_2022",
"kb-logging",
"kb_materializer_admin",
"kb_materializer_compliance_audit",
"kb_materializer_lifecycle",
"kb-lib.materializer.admin",
"kb-lib.materializer.compliance",
"kb-lib.materializer.lifecycle",
"kb_materializer_risk",
"kb_materializer_staking",
"kb-lib.materializer.staking",
"kb_materializer_token_accounts",
"kb_materializer_transaction_annotations",
"kb_pipeline",
@@ -693,120 +693,120 @@
]
},
{
"name": "file_kb_materializer_admin_debug",
"name": "file_kb_lib_materializer_admin_debug",
"enabled": true,
"sink": "file",
"level": "debug",
"path": "logs/devnet/kb_materializer_admin/debug.log",
"path": "logs/devnet/kb-lib/materializer/admin/debug.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"targets": [
"kb_materializer_admin"
"kb-lib.materializer.admin"
]
},
{
"name": "file_kb_materializer_admin_info",
"name": "file_kb_lib_materializer_admin_info",
"enabled": true,
"sink": "file",
"level": "info",
"path": "logs/devnet/kb_materializer_admin/info.log",
"path": "logs/devnet/kb-lib/materializer/admin/info.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"targets": [
"kb_materializer_admin"
"kb-lib.materializer.admin"
]
},
{
"name": "file_kb_materializer_admin_error",
"name": "file_kb_lib_materializer_admin_error",
"enabled": true,
"sink": "file",
"level": "error",
"path": "logs/devnet/kb_materializer_admin/error.jsonl",
"path": "logs/devnet/kb-lib/materializer/admin/error.jsonl",
"rotation": "daily",
"format": "json",
"ansi": false,
"targets": [
"kb_materializer_admin"
"kb-lib.materializer.admin"
]
},
{
"name": "file_kb_materializer_compliance_audit_debug",
"name": "file_kb_lib_materializer_compliance_debug",
"enabled": true,
"sink": "file",
"level": "debug",
"path": "logs/devnet/kb_materializer_compliance_audit/debug.log",
"path": "logs/devnet/kb-lib/materializer/compliance/debug.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"targets": [
"kb_materializer_compliance_audit"
"kb-lib.materializer.compliance"
]
},
{
"name": "file_kb_materializer_compliance_audit_info",
"name": "file_kb_lib_materializer_compliance_info",
"enabled": true,
"sink": "file",
"level": "info",
"path": "logs/devnet/kb_materializer_compliance_audit/info.log",
"path": "logs/devnet/kb-lib/materializer/compliance/info.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"targets": [
"kb_materializer_compliance_audit"
"kb-lib.materializer.compliance"
]
},
{
"name": "file_kb_materializer_compliance_audit_error",
"name": "file_kb_lib_materializer_compliance_error",
"enabled": true,
"sink": "file",
"level": "error",
"path": "logs/devnet/kb_materializer_compliance_audit/error.jsonl",
"path": "logs/devnet/kb-lib/materializer/compliance/error.jsonl",
"rotation": "daily",
"format": "json",
"ansi": false,
"targets": [
"kb_materializer_compliance_audit"
"kb-lib.materializer.compliance"
]
},
{
"name": "file_kb_materializer_lifecycle_debug",
"name": "file_kb_lib_materializer_lifecycle_debug",
"enabled": true,
"sink": "file",
"level": "debug",
"path": "logs/devnet/kb_materializer_lifecycle/debug.log",
"path": "logs/devnet/kb-lib/materializer/lifecycle/debug.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"targets": [
"kb_materializer_lifecycle"
"kb-lib.materializer.lifecycle"
]
},
{
"name": "file_kb_materializer_lifecycle_info",
"name": "file_kb_lib_materializer_lifecycle_info",
"enabled": true,
"sink": "file",
"level": "info",
"path": "logs/devnet/kb_materializer_lifecycle/info.log",
"path": "logs/devnet/kb-lib/materializer/lifecycle/info.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"targets": [
"kb_materializer_lifecycle"
"kb-lib.materializer.lifecycle"
]
},
{
"name": "file_kb_materializer_lifecycle_error",
"name": "file_kb_lib_materializer_lifecycle_error",
"enabled": true,
"sink": "file",
"level": "error",
"path": "logs/devnet/kb_materializer_lifecycle/error.jsonl",
"path": "logs/devnet/kb-lib/materializer/lifecycle/error.jsonl",
"rotation": "daily",
"format": "json",
"ansi": false,
"targets": [
"kb_materializer_lifecycle"
"kb-lib.materializer.lifecycle"
]
},
{
@@ -849,39 +849,39 @@
]
},
{
"name": "file_kb_materializer_staking_debug",
"name": "file_kb_lib_materializer_staking_debug",
"enabled": true,
"level": "debug",
"format": "human",
"path": "logs/devnet/kb_materializer_staking/debug.log",
"path": "logs/devnet/kb-lib/materializer/staking/debug.log",
"targets": [
"kb_materializer_staking"
"kb-lib.materializer.staking"
],
"sink": "file",
"rotation": "daily",
"ansi": false
},
{
"name": "file_kb_materializer_staking_info",
"name": "file_kb_lib_materializer_staking_info",
"enabled": true,
"level": "info",
"format": "human",
"path": "logs/devnet/kb_materializer_staking/info.log",
"path": "logs/devnet/kb-lib/materializer/staking/info.log",
"targets": [
"kb_materializer_staking"
"kb-lib.materializer.staking"
],
"sink": "file",
"rotation": "daily",
"ansi": false
},
{
"name": "file_kb_materializer_staking_error",
"name": "file_kb_lib_materializer_staking_error",
"enabled": true,
"level": "error",
"format": "json",
"path": "logs/devnet/kb_materializer_staking/error.jsonl",
"path": "logs/devnet/kb-lib/materializer/staking/error.jsonl",
"targets": [
"kb_materializer_staking"
"kb-lib.materializer.staking"
],
"sink": "file",
"rotation": "daily",
@@ -1644,11 +1644,11 @@
"kb_executor_spl_token",
"kb_executor_spl_token_2022",
"kb-logging",
"kb_materializer_admin",
"kb_materializer_compliance_audit",
"kb_materializer_lifecycle",
"kb-lib.materializer.admin",
"kb-lib.materializer.compliance",
"kb-lib.materializer.lifecycle",
"kb_materializer_risk",
"kb_materializer_staking",
"kb-lib.materializer.staking",
"kb_materializer_token_accounts",
"kb_materializer_transaction_annotations",
"kb_pipeline",
@@ -2295,120 +2295,120 @@
]
},
{
"name": "file_kb_materializer_admin_debug",
"name": "file_kb_lib_materializer_admin_debug",
"enabled": true,
"sink": "file",
"level": "debug",
"path": "logs/mainnet_research/kb_materializer_admin/debug.log",
"path": "logs/mainnet_research/kb-lib/materializer/admin/debug.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"targets": [
"kb_materializer_admin"
"kb-lib.materializer.admin"
]
},
{
"name": "file_kb_materializer_admin_info",
"name": "file_kb_lib_materializer_admin_info",
"enabled": true,
"sink": "file",
"level": "info",
"path": "logs/mainnet_research/kb_materializer_admin/info.log",
"path": "logs/mainnet_research/kb-lib/materializer/admin/info.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"targets": [
"kb_materializer_admin"
"kb-lib.materializer.admin"
]
},
{
"name": "file_kb_materializer_admin_error",
"name": "file_kb_lib_materializer_admin_error",
"enabled": true,
"sink": "file",
"level": "error",
"path": "logs/mainnet_research/kb_materializer_admin/error.jsonl",
"path": "logs/mainnet_research/kb-lib/materializer/admin/error.jsonl",
"rotation": "daily",
"format": "json",
"ansi": false,
"targets": [
"kb_materializer_admin"
"kb-lib.materializer.admin"
]
},
{
"name": "file_kb_materializer_compliance_audit_debug",
"name": "file_kb_lib_materializer_compliance_debug",
"enabled": true,
"sink": "file",
"level": "debug",
"path": "logs/mainnet_research/kb_materializer_compliance_audit/debug.log",
"path": "logs/mainnet_research/kb-lib/materializer/compliance/debug.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"targets": [
"kb_materializer_compliance_audit"
"kb-lib.materializer.compliance"
]
},
{
"name": "file_kb_materializer_compliance_audit_info",
"name": "file_kb_lib_materializer_compliance_info",
"enabled": true,
"sink": "file",
"level": "info",
"path": "logs/mainnet_research/kb_materializer_compliance_audit/info.log",
"path": "logs/mainnet_research/kb-lib/materializer/compliance/info.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"targets": [
"kb_materializer_compliance_audit"
"kb-lib.materializer.compliance"
]
},
{
"name": "file_kb_materializer_compliance_audit_error",
"name": "file_kb_lib_materializer_compliance_error",
"enabled": true,
"sink": "file",
"level": "error",
"path": "logs/mainnet_research/kb_materializer_compliance_audit/error.jsonl",
"path": "logs/mainnet_research/kb-lib/materializer/compliance/error.jsonl",
"rotation": "daily",
"format": "json",
"ansi": false,
"targets": [
"kb_materializer_compliance_audit"
"kb-lib.materializer.compliance"
]
},
{
"name": "file_kb_materializer_lifecycle_debug",
"name": "file_kb_lib_materializer_lifecycle_debug",
"enabled": true,
"sink": "file",
"level": "debug",
"path": "logs/mainnet_research/kb_materializer_lifecycle/debug.log",
"path": "logs/mainnet_research/kb-lib/materializer/lifecycle/debug.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"targets": [
"kb_materializer_lifecycle"
"kb-lib.materializer.lifecycle"
]
},
{
"name": "file_kb_materializer_lifecycle_info",
"name": "file_kb_lib_materializer_lifecycle_info",
"enabled": true,
"sink": "file",
"level": "info",
"path": "logs/mainnet_research/kb_materializer_lifecycle/info.log",
"path": "logs/mainnet_research/kb-lib/materializer/lifecycle/info.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"targets": [
"kb_materializer_lifecycle"
"kb-lib.materializer.lifecycle"
]
},
{
"name": "file_kb_materializer_lifecycle_error",
"name": "file_kb_lib_materializer_lifecycle_error",
"enabled": true,
"sink": "file",
"level": "error",
"path": "logs/mainnet_research/kb_materializer_lifecycle/error.jsonl",
"path": "logs/mainnet_research/kb-lib/materializer/lifecycle/error.jsonl",
"rotation": "daily",
"format": "json",
"ansi": false,
"targets": [
"kb_materializer_lifecycle"
"kb-lib.materializer.lifecycle"
]
},
{
@@ -2451,39 +2451,39 @@
]
},
{
"name": "file_kb_materializer_staking_debug",
"name": "file_kb_lib_materializer_staking_debug",
"enabled": true,
"level": "debug",
"format": "human",
"path": "logs/mainnet_research/kb_materializer_staking/debug.log",
"path": "logs/mainnet_research/kb-lib/materializer/staking/debug.log",
"targets": [
"kb_materializer_staking"
"kb-lib.materializer.staking"
],
"sink": "file",
"rotation": "daily",
"ansi": false
},
{
"name": "file_kb_materializer_staking_info",
"name": "file_kb_lib_materializer_staking_info",
"enabled": true,
"level": "info",
"format": "human",
"path": "logs/mainnet_research/kb_materializer_staking/info.log",
"path": "logs/mainnet_research/kb-lib/materializer/staking/info.log",
"targets": [
"kb_materializer_staking"
"kb-lib.materializer.staking"
],
"sink": "file",
"rotation": "daily",
"ansi": false
},
{
"name": "file_kb_materializer_staking_error",
"name": "file_kb_lib_materializer_staking_error",
"enabled": true,
"level": "error",
"format": "json",
"path": "logs/mainnet_research/kb_materializer_staking/error.jsonl",
"path": "logs/mainnet_research/kb-lib/materializer/staking/error.jsonl",
"targets": [
"kb_materializer_staking"
"kb-lib.materializer.staking"
],
"sink": "file",
"rotation": "daily",
@@ -3446,11 +3446,11 @@
"kb_executor_spl_token",
"kb_executor_spl_token_2022",
"kb-logging",
"kb_materializer_admin",
"kb_materializer_compliance_audit",
"kb_materializer_lifecycle",
"kb-lib.materializer.admin",
"kb-lib.materializer.compliance",
"kb-lib.materializer.lifecycle",
"kb_materializer_risk",
"kb_materializer_staking",
"kb-lib.materializer.staking",
"kb_materializer_token_accounts",
"kb_materializer_transaction_annotations",
"kb_pipeline",
@@ -4097,120 +4097,120 @@
]
},
{
"name": "file_kb_materializer_admin_debug",
"name": "file_kb_lib_materializer_admin_debug",
"enabled": true,
"sink": "file",
"level": "debug",
"path": "logs/mainnet/kb_materializer_admin/debug.log",
"path": "logs/mainnet/kb-lib/materializer/admin/debug.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"targets": [
"kb_materializer_admin"
"kb-lib.materializer.admin"
]
},
{
"name": "file_kb_materializer_admin_info",
"name": "file_kb_lib_materializer_admin_info",
"enabled": true,
"sink": "file",
"level": "info",
"path": "logs/mainnet/kb_materializer_admin/info.log",
"path": "logs/mainnet/kb-lib/materializer/admin/info.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"targets": [
"kb_materializer_admin"
"kb-lib.materializer.admin"
]
},
{
"name": "file_kb_materializer_admin_error",
"name": "file_kb_lib_materializer_admin_error",
"enabled": true,
"sink": "file",
"level": "error",
"path": "logs/mainnet/kb_materializer_admin/error.jsonl",
"path": "logs/mainnet/kb-lib/materializer/admin/error.jsonl",
"rotation": "daily",
"format": "json",
"ansi": false,
"targets": [
"kb_materializer_admin"
"kb-lib.materializer.admin"
]
},
{
"name": "file_kb_materializer_compliance_audit_debug",
"name": "file_kb_lib_materializer_compliance_debug",
"enabled": true,
"sink": "file",
"level": "debug",
"path": "logs/mainnet/kb_materializer_compliance_audit/debug.log",
"path": "logs/mainnet/kb-lib/materializer/compliance/debug.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"targets": [
"kb_materializer_compliance_audit"
"kb-lib.materializer.compliance"
]
},
{
"name": "file_kb_materializer_compliance_audit_info",
"name": "file_kb_lib_materializer_compliance_info",
"enabled": true,
"sink": "file",
"level": "info",
"path": "logs/mainnet/kb_materializer_compliance_audit/info.log",
"path": "logs/mainnet/kb-lib/materializer/compliance/info.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"targets": [
"kb_materializer_compliance_audit"
"kb-lib.materializer.compliance"
]
},
{
"name": "file_kb_materializer_compliance_audit_error",
"name": "file_kb_lib_materializer_compliance_error",
"enabled": true,
"sink": "file",
"level": "error",
"path": "logs/mainnet/kb_materializer_compliance_audit/error.jsonl",
"path": "logs/mainnet/kb-lib/materializer/compliance/error.jsonl",
"rotation": "daily",
"format": "json",
"ansi": false,
"targets": [
"kb_materializer_compliance_audit"
"kb-lib.materializer.compliance"
]
},
{
"name": "file_kb_materializer_lifecycle_debug",
"name": "file_kb_lib_materializer_lifecycle_debug",
"enabled": true,
"sink": "file",
"level": "debug",
"path": "logs/mainnet/kb_materializer_lifecycle/debug.log",
"path": "logs/mainnet/kb-lib/materializer/lifecycle/debug.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"targets": [
"kb_materializer_lifecycle"
"kb-lib.materializer.lifecycle"
]
},
{
"name": "file_kb_materializer_lifecycle_info",
"name": "file_kb_lib_materializer_lifecycle_info",
"enabled": true,
"sink": "file",
"level": "info",
"path": "logs/mainnet/kb_materializer_lifecycle/info.log",
"path": "logs/mainnet/kb-lib/materializer/lifecycle/info.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"targets": [
"kb_materializer_lifecycle"
"kb-lib.materializer.lifecycle"
]
},
{
"name": "file_kb_materializer_lifecycle_error",
"name": "file_kb_lib_materializer_lifecycle_error",
"enabled": true,
"sink": "file",
"level": "error",
"path": "logs/mainnet/kb_materializer_lifecycle/error.jsonl",
"path": "logs/mainnet/kb-lib/materializer/lifecycle/error.jsonl",
"rotation": "daily",
"format": "json",
"ansi": false,
"targets": [
"kb_materializer_lifecycle"
"kb-lib.materializer.lifecycle"
]
},
{
@@ -4253,39 +4253,39 @@
]
},
{
"name": "file_kb_materializer_staking_debug",
"name": "file_kb_lib_materializer_staking_debug",
"enabled": true,
"level": "debug",
"format": "human",
"path": "logs/mainnet/kb_materializer_staking/debug.log",
"path": "logs/mainnet/kb-lib/materializer/staking/debug.log",
"targets": [
"kb_materializer_staking"
"kb-lib.materializer.staking"
],
"sink": "file",
"rotation": "daily",
"ansi": false
},
{
"name": "file_kb_materializer_staking_info",
"name": "file_kb_lib_materializer_staking_info",
"enabled": true,
"level": "info",
"format": "human",
"path": "logs/mainnet/kb_materializer_staking/info.log",
"path": "logs/mainnet/kb-lib/materializer/staking/info.log",
"targets": [
"kb_materializer_staking"
"kb-lib.materializer.staking"
],
"sink": "file",
"rotation": "daily",
"ansi": false
},
{
"name": "file_kb_materializer_staking_error",
"name": "file_kb_lib_materializer_staking_error",
"enabled": true,
"level": "error",
"format": "json",
"path": "logs/mainnet/kb_materializer_staking/error.jsonl",
"path": "logs/mainnet/kb-lib/materializer/staking/error.jsonl",
"targets": [
"kb_materializer_staking"
"kb-lib.materializer.staking"
],
"sink": "file",
"rotation": "daily",

View File

@@ -1,5 +1,5 @@
<!-- file: docs/LOGGING.md -->
<!-- version: 8 -->
<!-- version: 9 -->
# Logging
@@ -40,7 +40,7 @@ Les niveaux sont cumulatifs :
## Targets canoniques
Le target est exactement le nom de la crate et provient de `src/constants.rs` :
Le target est le nom exact de la crate hors `kb-lib`. Dans `kb-lib`, il suit la hiérarchie du composant :
```text
kb_app_demo
@@ -52,10 +52,10 @@ kb_executor_spl_memo
kb_executor_spl_noop
kb_executor_spl_single_pool
kb_logging
kb_materializer_admin
kb_materializer_compliance_audit
kb_materializer_lifecycle
kb_materializer_staking
kb-lib.materializer.admin
kb-lib.materializer.compliance
kb-lib.materializer.lifecycle
kb-lib.materializer.staking
kb_pipeline
kb_rpc
kb_store_pg

View File

@@ -1,5 +1,5 @@
<!-- file: docs/MATERIALIZATIONS.md -->
<!-- version: 6 -->
<!-- version: 7 -->
# Catalogue des matérialisations
@@ -18,9 +18,9 @@ Les matérialisations transforment des événements décodés en événements m
- `reward` : émission, claim, distribution, farming, incentives.
- `risk` : signaux dérivés à partir des événements admin, liquidité, metadata, authority ou anomalies.
## Projections natives actives dans `0.4.1`
## Projections natives portées dans `kb-lib`
`kb_materializer_lifecycle` produit des événements lifecycle idempotents à partir d'observations exactes, réussies et commitées :
`kb_lib::LifecycleMaterializer` produit des événements lifecycle idempotents à partir d'observations exactes, réussies et commitées :
| Domaine | Sortie stable | Entrées couvertes |
|----------------------|--------------------------------------------|--------------------------------------------------------------------------------------------------------------|
@@ -34,7 +34,9 @@ Les matérialisations transforment des événements décodés en événements m
La famille décodée n'impose pas à elle seule la famille matérialisée. `authorize_nonce_account` est un événement `Admin`, tandis qu'une vérification ZK ElGamal est un événement `Audit`; ces observations ne produisent une sortie `Lifecycle` que lorsque leur mutation durable exacte est reconnue. Une transaction échouée ou non commitée est toujours refusée.
`kb_materializer_admin` produit des sorties `Admin` pour les assignations System, les écritures Config opaques et les changements dautorité Loader. `kb_materializer_compliance_audit` produit des sorties `ComplianceAudit` pour les écritures/copies de bytecode Loader. `kb_materializer_staking` produit les projections instructionnelles Stake/Vote commitées : comptes stake/vote, autorités, lockup, délégation, vote state, retraits et dépôt de rewards. Les données complètes ne sont jamais recopiées : seules les clés, autorités, offsets, tailles, hashes, préfixes et paramètres bornés disponibles sont conservés.
`kb_lib::AdminMaterializer` produit des sorties `Admin` pour les assignations System, les écritures Config opaques et les changements dautorité Loader. `kb_lib::ComplianceAuditMaterializer` produit des sorties `ComplianceAudit` pour les écritures/copies de bytecode Loader. `kb_lib::StakingMaterializer` produit les projections instructionnelles Stake/Vote commitées : comptes stake/vote, autorités, lockup, délégation, vote state, retraits et dépôt de rewards. Les données complètes ne sont jamais recopiées : seules les clés, autorités, offsets, tailles, hashes, préfixes et paramètres bornés disponibles sont conservés.
Les quatre implémentations résident respectivement dans `kb_lib::materializer::lifecycle`, `admin`, `compliance` et `staking`, mais sont réexportées à la racine de `kb-lib`. Les identités de processor historiques restent stables pour les replays ; les targets de tracing utilisent la hiérarchie consolidée `kb-lib.materializer.*`.
Le retrait d'un nonce account conserve une sémantique conditionnelle : le runtime ferme le compte uniquement lorsque la totalité du solde est retirée. La projection décrit l'opération commitée et ne prétend pas reconstruire l'état final transactionnel du compte. Les deltas SOL restent la responsabilité du graphe core.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/NATIVE_SOLANA_MATERIALIZATION_AUDIT.md -->
<!-- version: 5 -->
<!-- version: 6 -->
# Audit des matérialisations Solana natives
@@ -13,19 +13,19 @@ Les sorties ne sont pas des snapshots finaux de comptes ou de runtime. Les état
| Matérialiseur | Surfaces | Projection |
|------------------------------------|----------------------|---------------------------------------------------------------------------------------------------------------------|
| `kb_materializer_lifecycle` | Address Lookup Table | lifecycle create/freeze/extend/deactivate/close |
| `kb_materializer_lifecycle` | Feature | révocation de feature gate |
| `kb_materializer_lifecycle` | Loaders | déploiement, finalisation et transitions lifecycle stables |
| `kb_materializer_lifecycle` | System | durable nonce, create account et allocate |
| `kb_materializer_lifecycle` | ZK ElGamal | création ou fermeture dun compte de contexte |
| `kb_materializer_lifecycle` | Slashing | initialisation ou fermeture dun rapport de violation |
| `kb_materializer_admin` | System | assign et assign with seed |
| `kb_materializer_admin` | Config | écriture opaque bornée avec clés, taille, SHA-256 et préfixe |
| `kb_materializer_admin` | Loaders | changements dautorité |
| `kb_materializer_compliance_audit` | Loaders | écritures/copies de bytecode bornées par offsets, tailles, SHA-256 et préfixes |
| `kb_materializer_compliance_audit` | Compute Budget | profil transactionnel agrégé, last-write-wins, une seule sortie par transaction |
| `kb_materializer_staking` | Stake | compte, délégation, désactivation, split, merge, withdraw, move stake/lamports, autorités et lockup instructionnels |
| `kb_materializer_staking` | Vote | compte, autorités, identité, commission, vote state, retraits et dépôt de rewards instructionnels |
| `LifecycleMaterializer` | Address Lookup Table | lifecycle create/freeze/extend/deactivate/close |
| `LifecycleMaterializer` | Feature | révocation de feature gate |
| `LifecycleMaterializer` | Loaders | déploiement, finalisation et transitions lifecycle stables |
| `LifecycleMaterializer` | System | durable nonce, create account et allocate |
| `LifecycleMaterializer` | ZK ElGamal | création ou fermeture dun compte de contexte |
| `LifecycleMaterializer` | Slashing | initialisation ou fermeture dun rapport de violation |
| `AdminMaterializer` | System | assign et assign with seed |
| `AdminMaterializer` | Config | écriture opaque bornée avec clés, taille, SHA-256 et préfixe |
| `AdminMaterializer` | Loaders | changements dautorité |
| `ComplianceAuditMaterializer` | Loaders | écritures/copies de bytecode bornées par offsets, tailles, SHA-256 et préfixes |
| `ComplianceAuditMaterializer` | Compute Budget | profil transactionnel agrégé, last-write-wins, une seule sortie par transaction |
| `StakingMaterializer` | Stake | compte, délégation, désactivation, split, merge, withdraw, move stake/lamports, autorités et lockup instructionnels |
| `StakingMaterializer` | Vote | compte, autorités, identité, commission, vote state, retraits et dépôt de rewards instructionnels |
## Politique des transactions échouées

View File

@@ -1,7 +1,7 @@
<!-- file: docs/SURFACE_CRATE_MATRIX.md -->
<!-- version: 4 -->
<!-- version: 5 -->
# Matrice des crates réservés
# Matrice des modules réservés
Ce document liste les crates actuellement présents dans le workspace. Un crate réservé ne signifie pas que le décodage, la matérialisation ou lexécution est validé.
@@ -246,27 +246,28 @@ Ce document liste les crates actuellement présents dans le workspace. Un crate
## Matérialisateurs
| Crate |
|---------------------------------------|
| `kb_materializer_admin` |
| `kb_materializer_bridge` |
| `kb_materializer_compliance_audit` |
| `kb_materializer_fees` |
| `kb_materializer_governance` |
| `kb_materializer_lending` |
| `kb_materializer_lifecycle` |
| `kb_materializer_liquidity` |
| `kb_materializer_metadata` |
| `kb_materializer_nft` |
| `kb_materializer_oracle` |
| `kb_materializer_orderbook` |
| `kb_materializer_perpetuals` |
| `kb_materializer_pool_state` |
| `kb_materializer_rewards` |
| `kb_materializer_risk` |
| `kb_materializer_routing` |
| `kb_materializer_staking` |
| `kb_materializer_token_accounts` |
| `kb_materializer_token_metadata_risk` |
| `kb_materializer_trades` |
| `kb_materializer_vault` |
| Module `kb-lib` |
|-----------------------------------------------|
| `materializer::admin::core` |
| `materializer::bridge::core` |
| `materializer::compliance::audit` |
| `materializer::fees::core` |
| `materializer::governance::core` |
| `materializer::lending::core` |
| `materializer::lifecycle::core` |
| `materializer::liquidity::core` |
| `materializer::metadata::core` |
| `materializer::nft::core` |
| `materializer::oracle::core` |
| `materializer::orderbook::core` |
| `materializer::perpetuals::core` |
| `materializer::pool::state` |
| `materializer::rewards::core` |
| `materializer::risk::core` |
| `materializer::routing::core` |
| `materializer::staking::core` |
| `materializer::token::accounts` |
| `materializer::token::metadata_risk` |
| `materializer::trades::core` |
| `materializer::transaction::annotations` |
| `materializer::vault::core` |

View File

@@ -1,5 +1,5 @@
<!-- file: docs/TRACING_CONTRACT.md -->
<!-- version: 18 -->
<!-- version: 19 -->
# Contrat de tracing par crate et composant
@@ -63,6 +63,10 @@ kb-lib.decoder.spl.elgamal_registry
kb-lib.decoder.spl.memo
kb-lib.decoder.spl.token
kb-lib.decoder.spl.token_2022
kb-lib.materializer.admin
kb-lib.materializer.compliance
kb-lib.materializer.lifecycle
kb-lib.materializer.staking
kb-logging
kb-store
```

View File

@@ -1,5 +1,5 @@
<!-- file: kb-lib/README.md -->
<!-- version: 6 -->
<!-- version: 7 -->
# kb-lib
@@ -100,6 +100,9 @@ politique dexécution définitivement interdite. La matrice normative est
## API publique utile
- `EventMaterializer`, `MaterializerIdentity`, `MaterializationTransactionPolicy`, `MaterializerExecutionResult` et `MaterializedOutput` : contrat complet utilisable par un matérialiseur interne ou externe ;
- `LifecycleMaterializer`, `AdminMaterializer`, `ComplianceAuditMaterializer` et `StakingMaterializer` : matérialisateurs Solana natifs concrets ;
- `materialize_token_2022_admin_state_snapshots()` et `materialize_elgamal_registry_state_snapshot()` : projections stateful dadministration déjà exposées ;
- `SolanaCoreDecoder` : décodeur concret natif ;
- `SplMemoDecoder` : décodeur exact des trois générations SPL Memo ;
- `SplTokenDecoder` : décodeur exact du programme SPL Token classique ;
@@ -131,4 +134,5 @@ Lappel de décodage complet utilise `InstructionDecoder::decode` après une r
- `kb-lib` ne dépend jamais de `kb-store`.
- Les décodeurs ne lisent pas un état RPC courant pour reconstruire une transaction historique.
- Une transaction échouée peut produire une intention structurée, jamais une mutation commitée.
- Les matérialisateurs et exécuteurs seront portés dans des tranches séparées.
- Un matérialiseur externe dépend uniquement des contrats et modèles publics de `kb-lib`, jamais dun décodeur concret.
- Les autres matérialisateurs et les exécuteurs sont portés dans des tranches séparées.

View File

@@ -1,5 +1,5 @@
// file: kb-lib/src/lib.rs
// version: 11
// version: 12
//! Consolidated decoder, executor, materializer and shared model library.
#![warn(missing_docs)]
@@ -286,6 +286,16 @@ pub(crate) use crate::decoder::spl_token_decode;
pub(crate) use crate::decoder::spl_token_entry_for_tag;
/// Returns the first byte of one retained classic SPL Token payload.
pub(crate) use crate::decoder::spl_token_payload_tag;
/// Canonical tracing target for the native and SPL administration materializer.
pub(crate) use crate::materializer::admin::MATERIALIZER_ADMIN_TRACING_TARGET;
/// Canonical processor name for the native and SPL administration materializer.
pub(crate) use crate::materializer::admin::PROCESSOR_NAME as MATERIALIZER_ADMIN_PROCESSOR_NAME;
/// Canonical tracing target for the native compliance audit materializer.
pub(crate) use crate::materializer::compliance::MATERIALIZER_COMPLIANCE_AUDIT_TRACING_TARGET;
/// Canonical tracing target for the native lifecycle materializer.
pub(crate) use crate::materializer::lifecycle::MATERIALIZER_LIFECYCLE_TRACING_TARGET;
/// Canonical tracing target for the native stake and vote materializer.
pub(crate) use crate::materializer::staking::MATERIALIZER_STAKING_TRACING_TARGET;
/// Exact byte length of one SPL ElGamal registry account.
pub use crate::decoder::ELGAMAL_REGISTRY_ACCOUNT_LEN;
@@ -405,6 +415,12 @@ pub use crate::executor::api::executor::InstructionExecutor;
pub use crate::executor::api::executor::serialize_payload_json;
/// Exposes the pretty JSON payload serializer.
pub use crate::executor::api::executor::serialize_payload_json_pretty;
/// Stable native and SPL administration materializer.
pub use crate::materializer::admin::AdminMaterializer;
/// Materializes one exact SPL ElGamal registry account snapshot.
pub use crate::materializer::admin::materialize_elgamal_registry_state_snapshot;
/// Materializes exact Token-2022 administration account snapshots.
pub use crate::materializer::admin::materialize_token_2022_admin_state_snapshots;
/// Stable decoded observation materializer contract used by the common pipeline.
pub use crate::materializer::api::contracts::EventMaterializer;
/// Explicit policy applied to successful and failed source transactions.
@@ -429,6 +445,12 @@ pub use crate::materializer::api::contracts::validate_materialization_policy;
pub use crate::materializer::api::contracts::validate_materialized_output_policy;
/// Exposes the legacy common business materializer trait.
pub use crate::materializer::api::materializer::Materializer;
/// Stable native compliance audit materializer.
pub use crate::materializer::compliance::ComplianceAuditMaterializer;
/// Stable native lifecycle materializer.
pub use crate::materializer::lifecycle::LifecycleMaterializer;
/// Stable native stake and vote materializer.
pub use crate::materializer::staking::StakingMaterializer;
/// Current canonical transaction document version.
pub use crate::model::canonical_transaction::CANONICAL_TRANSACTION_FORMAT_VERSION;
/// Canonical address lookup table reference.

View File

@@ -1,6 +1,17 @@
// file: kb-lib/src/materializer/admin.rs
// version: 1
// version: 2
//! `admin` materializer family.
mod constants;
pub mod core;
pub(crate) use crate::materializer::admin::constants::PROCESSOR_NAME;
pub(crate) use crate::materializer::admin::constants::TRACING_TARGET as MATERIALIZER_ADMIN_TRACING_TARGET;
/// Stable native and SPL administration materializer.
pub use crate::materializer::admin::core::AdminMaterializer;
/// Materializes one exact SPL ElGamal registry account snapshot.
pub use crate::materializer::admin::core::materialize_elgamal_registry_state_snapshot;
/// Materializes exact Token-2022 administration account snapshots.
pub use crate::materializer::admin::core::materialize_token_2022_admin_state_snapshots;

View File

@@ -0,0 +1,10 @@
// file: kb-lib/src/materializer/admin/constants.rs
// version: 1
//! Local constants for the native and SPL administration materializer.
/// Canonical processor name for this component.
pub(crate) const PROCESSOR_NAME: &str = "solana_native_admin";
/// Canonical tracing target for this component.
pub(crate) const TRACING_TARGET: &str = "kb-lib.materializer.admin";

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,12 @@
// file: kb-lib/src/materializer/compliance.rs
// version: 1
// version: 2
//! `compliance` materializer family.
pub mod audit;
mod constants;
pub(crate) use crate::materializer::compliance::constants::TRACING_TARGET as MATERIALIZER_COMPLIANCE_AUDIT_TRACING_TARGET;
/// Stable native compliance audit materializer.
pub use crate::materializer::compliance::audit::ComplianceAuditMaterializer;

View File

@@ -1,10 +1,577 @@
// file: kb-lib/src/materializer/compliance/audit.rs
// version: 1
// version: 7
//! Migration boundary for legacy crate `kb_materializer_compliance_audit`.
//! Stable compliance-audit projections for committed native bytecode mutations and execution profiles.
/// Legacy crate name retained for migration and compatibility tracking.
pub const LEGACY_CRATE: &str = "kb_materializer_compliance_audit";
const ACCEPTED_FAMILIES: &[crate::EventFamily] = &[crate::EventFamily::Audit];
const COMPUTE_BUDGET_SURFACE: &str = "solana_native_compute_budget";
const COMPUTE_BUDGET_ENTRIES: &[&str] = &[
"request_units_deprecated",
"request_heap_frame",
"set_compute_unit_limit",
"set_compute_unit_price",
"set_loaded_accounts_data_size_limit",
];
const BPF_LOADER_DEPRECATED_SURFACE: &str = "solana_native_bpf_loader_deprecated";
const BPF_LOADER_SURFACE: &str = "solana_native_bpf_loader";
const BPF_LOADER_UPGRADEABLE_SURFACE: &str = "solana_native_bpf_loader_upgradeable";
const LOADER_V4_SURFACE: &str = "solana_native_loader_v4";
const WRITE_ENTRIES: &[&str] = &["write"];
const LOADER_V4_COPY_ENTRIES: &[&str] = &["copy"];
/// Current porting status.
pub const MIGRATION_STATUS: &str = "source-preserved-pending-port";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum ProjectionKind {
ProgramByteWrite(&'static str),
ProgramByteCopy,
ComputeBudgetProfile,
}
impl ProjectionKind {
fn output_key(self, surface_code: &str, operation: &str) -> std::string::String {
return match self {
Self::ProgramByteWrite(_) | Self::ProgramByteCopy => {
format!("program_loader_code:{surface_code}:{operation}:0")
},
Self::ComputeBudgetProfile => "transaction_compute_budget_profile:0".to_string(),
};
}
fn loader_surface(self) -> &'static str {
return match self {
Self::ProgramByteWrite(surface) => surface,
Self::ProgramByteCopy => LOADER_V4_SURFACE,
Self::ComputeBudgetProfile => COMPUTE_BUDGET_SURFACE,
};
}
}
/// Stable compliance-audit materializer for native bytecode mutations and execution profiles.
#[derive(Clone, Debug, Default)]
pub struct ComplianceAuditMaterializer;
impl crate::Materializer for crate::ComplianceAuditMaterializer {
fn materializer_name(&self) -> &'static str {
return "kb_materializer_compliance_audit";
}
fn materializer_version(&self) -> &'static str {
return env!("CARGO_PKG_VERSION");
}
fn accepts_event(&self, event: &crate::DecodedProtocolEvent) -> bool {
return event.event_family == crate::EventFamily::Audit
&& projection(event.surface_code.0.as_str(), event.event_name.0.as_str()).is_some();
}
fn materialize_event(
&self,
event: &crate::DecodedProtocolEvent,
) -> kb_core::Result<std::vec::Vec<crate::MaterializedEvent>> {
if !crate::Materializer::accepts_event(self, event) {
return std::result::Result::Ok(std::vec::Vec::new());
}
return std::result::Result::Ok(std::vec::Vec::new());
}
}
impl crate::EventMaterializer for crate::ComplianceAuditMaterializer {
fn identity(&self) -> crate::MaterializerIdentity {
return crate::MaterializerIdentity {
name: "solana_native_compliance_audit".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
};
}
fn accepted_families(&self) -> &'static [crate::EventFamily] {
return ACCEPTED_FAMILIES;
}
fn accepts_observation(&self, observation: &crate::DecodedObservation) -> bool {
return observation.event.event_family == crate::EventFamily::Audit
&& projection(
observation.event.surface_code.0.as_str(),
observation.event.event_name.0.as_str(),
)
.is_some();
}
fn transaction_policy(
&self,
_family: crate::EventFamily,
) -> crate::MaterializationTransactionPolicy {
return crate::MaterializationTransactionPolicy::SuccessfulOrFailedAudit;
}
fn materialize(
&self,
observation: &crate::DecodedObservation,
) -> crate::MaterializerExecutionResult {
let projection = match projection(
observation.event.surface_code.0.as_str(),
observation.event.event_name.0.as_str(),
) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
tracing::debug!(
target: crate::MATERIALIZER_COMPLIANCE_AUDIT_TRACING_TARGET,
action = "materialize_compliance_audit",
surface_code = %observation.event.surface_code.0.as_str(),
entry_code = %observation.event.event_name.0.as_str(),
accepted = false,
"ignore observation outside supported native compliance-audit projections"
);
return crate::MaterializerExecutionResult::ignored();
},
};
if observation.event.event_family != crate::EventFamily::Audit {
return crate::MaterializerExecutionResult::ignored();
}
if projection != ProjectionKind::ComputeBudgetProfile
&& (observation.transaction_failed || !observation.observation_committed)
{
tracing::debug!(
target: crate::MATERIALIZER_COMPLIANCE_AUDIT_TRACING_TARGET,
action = "materialize_compliance_audit",
surface_code = %observation.event.surface_code.0.as_str(),
entry_code = %observation.event.event_name.0.as_str(),
committed = observation.observation_committed,
transaction_failed = observation.transaction_failed,
"refuse uncommitted native bytecode mutation observation"
);
return crate::MaterializerExecutionResult::refused(
"failed_transaction_compliance_audit_refused",
"failed or uncommitted native observations cannot create committed bytecode mutation outputs",
);
}
let accounts = observation
.payload_json
.get("accounts")
.cloned()
.unwrap_or(serde_json::Value::Null);
let parameters = observation
.payload_json
.get("parameters")
.cloned()
.unwrap_or(serde_json::Value::Null);
let operation = observation.event.event_name.0.clone();
if projection == ProjectionKind::ComputeBudgetProfile {
return materialize_compute_budget_profile(observation, parameters, operation);
}
let output = crate::MaterializedOutput {
output_key: projection
.output_key(observation.event.surface_code.0.as_str(), operation.as_str()),
family: crate::MaterializedEventFamily::ComplianceAudit,
payload_json: serde_json::json!({
"projectionVersion": 1,
"projectionSemantics": "committed_instruction_compliance_audit_event",
"domain": "program_loader_code_mutation",
"loaderSurface": projection.loader_surface(),
"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(),
"transactionSucceeded": true,
"accounts": accounts,
"parameters": parameters,
"projection": projection_details(projection, &accounts, &parameters),
}),
};
tracing::debug!(
target: crate::MATERIALIZER_COMPLIANCE_AUDIT_TRACING_TARGET,
action = "materialize_compliance_audit",
surface_code = %observation.event.surface_code.0.as_str(),
entry_code = %observation.event.event_name.0.as_str(),
loader_surface = projection.loader_surface(),
output_count = 1_usize,
"materialize committed native bytecode mutation observation"
);
return crate::MaterializerExecutionResult {
status: crate::MaterializerOutcomeStatus::Inserted,
outputs: std::vec![output],
diagnostics: std::vec::Vec::new(),
};
}
}
fn materialize_compute_budget_profile(
observation: &crate::DecodedObservation,
parameters: serde_json::Value,
operation: std::string::String,
) -> crate::MaterializerExecutionResult {
let profile = parameters.get("transactionProfile").cloned().unwrap_or(serde_json::Value::Null);
if !compute_budget_profile_emits(&profile) {
return crate::MaterializerExecutionResult::ignored();
}
let output = crate::MaterializedOutput {
output_key: "transaction_compute_budget_profile:0".to_string(),
family: crate::MaterializedEventFamily::ComplianceAudit,
payload_json: serde_json::json!({
"projectionVersion": 1,
"projectionSemantics": "transaction_compute_budget_profile",
"domain": "transaction_compute_budget_profile",
"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(),
"transactionSucceeded": !observation.transaction_failed,
"profile": profile,
"profileSource": {
"surfaceCode": observation.event.surface_code.0.clone(),
"entryCode": observation.event.event_name.0.clone(),
"eventKey": observation.event_key.clone()
},
}),
};
tracing::debug!(
target: crate::MATERIALIZER_COMPLIANCE_AUDIT_TRACING_TARGET,
action = "materialize_compute_budget_profile",
surface_code = %observation.event.surface_code.0.as_str(),
entry_code = %observation.event.event_name.0.as_str(),
output_count = 1_usize,
transaction_failed = observation.transaction_failed,
"materialize native Compute Budget transaction profile"
);
return crate::MaterializerExecutionResult {
status: crate::MaterializerOutcomeStatus::Inserted,
outputs: std::vec![output],
diagnostics: std::vec::Vec::new(),
};
}
fn compute_budget_profile_emits(profile: &serde_json::Value) -> bool {
return profile
.get("currentInstructionIsProfileEmitter")
.and_then(serde_json::Value::as_bool)
== std::option::Option::Some(true);
}
fn projection(surface_code: &str, entry_code: &str) -> std::option::Option<ProjectionKind> {
if surface_code == COMPUTE_BUDGET_SURFACE && contains(COMPUTE_BUDGET_ENTRIES, entry_code) {
return std::option::Option::Some(ProjectionKind::ComputeBudgetProfile);
}
if contains(WRITE_ENTRIES, entry_code) {
for supported in [
BPF_LOADER_DEPRECATED_SURFACE,
BPF_LOADER_SURFACE,
BPF_LOADER_UPGRADEABLE_SURFACE,
LOADER_V4_SURFACE,
] {
if surface_code == supported {
return std::option::Option::Some(ProjectionKind::ProgramByteWrite(supported));
}
}
}
if surface_code == LOADER_V4_SURFACE && contains(LOADER_V4_COPY_ENTRIES, entry_code) {
return std::option::Option::Some(ProjectionKind::ProgramByteCopy);
}
return std::option::Option::None;
}
fn projection_details(
projection: ProjectionKind,
accounts: &serde_json::Value,
parameters: &serde_json::Value,
) -> serde_json::Value {
return match projection {
ProjectionKind::ProgramByteWrite(_) => serde_json::json!({
"targetAccount": first_account_key_for_roles(
accounts,
&["program_account", "buffer_account"],
),
"authorityAccount": first_account_key_for_roles(
accounts,
&["authority", "buffer_authority"],
),
"offset": parameters.get("offset").cloned().unwrap_or(serde_json::Value::Null),
"byteLength": parameters.get("byteLength").cloned().unwrap_or(serde_json::Value::Null),
"bytesSha256": parameters.get("bytesSha256").cloned().unwrap_or(serde_json::Value::Null),
"bytesPrefixHex": parameters.get("bytesPrefixHex").cloned().unwrap_or(serde_json::Value::Null),
"stateTransition": "program_bytes_written",
"codeBytesMaterialized": false,
"transactionFinalAccountStateCaptured": false,
}),
ProjectionKind::ProgramByteCopy => serde_json::json!({
"targetAccount": account_key_for_role(accounts, "program_account"),
"authorityAccount": account_key_for_role(accounts, "authority"),
"sourceProgramAccount": account_key_for_role(accounts, "source_program_account"),
"destinationOffset": parameters.get("destinationOffset").cloned().unwrap_or(serde_json::Value::Null),
"sourceOffset": parameters.get("sourceOffset").cloned().unwrap_or(serde_json::Value::Null),
"byteLength": parameters.get("length").cloned().unwrap_or(serde_json::Value::Null),
"stateTransition": "program_bytes_copied",
"codeBytesMaterialized": false,
"transactionFinalAccountStateCaptured": false,
}),
ProjectionKind::ComputeBudgetProfile => serde_json::Value::Null,
};
}
fn first_account_key_for_roles(accounts: &serde_json::Value, roles: &[&str]) -> serde_json::Value {
for role in roles {
let value = account_key_for_role(accounts, role);
if !value.is_null() {
return value;
}
}
return serde_json::Value::Null;
}
fn account_key_for_role(accounts: &serde_json::Value, role: &str) -> serde_json::Value {
let account_key = accounts.as_array().and_then(|values| {
return values.iter().find_map(|value| {
if value.get("role").and_then(serde_json::Value::as_str)
!= std::option::Option::Some(role)
{
return std::option::Option::None;
}
return value
.get("accountKey")
.and_then(serde_json::Value::as_str)
.map(|account_key| return account_key.to_string());
});
});
return match account_key {
std::option::Option::Some(value) => serde_json::Value::String(value),
std::option::Option::None => serde_json::Value::Null,
};
}
fn contains(entries: &[&str], entry_code: &str) -> bool {
return entries.iter().any(|entry| return *entry == entry_code);
}
#[cfg(test)]
mod tests {
fn observation(
surface_code: &str,
entry_code: &str,
transaction_failed: bool,
accounts: serde_json::Value,
parameters: serde_json::Value,
) -> crate::DecodedObservation {
return crate::DecodedObservation {
event_key: format!("{entry_code}:0"),
event: crate::DecodedProtocolEvent {
signature: crate::Signature("signature".to_string()),
slot: crate::Slot(42),
instruction_path: crate::InstructionPath("0".to_string()),
program_id: crate::ProgramId("program-id".to_string()),
protocol_code: crate::ProtocolCode("solana_native".to_string()),
surface_code: crate::SurfaceCode(surface_code.to_string()),
event_code: crate::EventCode(format!("{surface_code}.{entry_code}")),
event_name: crate::EventName(entry_code.to_string()),
event_family: crate::EventFamily::Audit,
source_kind: crate::EventSourceKind::Instruction,
confidence: crate::DecoderConfidence::ManualExact,
},
payload_json: serde_json::json!({"accounts": accounts, "parameters": parameters}),
transaction_failed,
transaction_error: if transaction_failed {
std::option::Option::Some(serde_json::json!({"InstructionError": [0, "Custom"]}))
} else {
std::option::Option::None
},
observation_committed: !transaction_failed,
proof: crate::DecoderProof {
kind: crate::DecoderProofKind::Manual,
confidence: crate::DecoderConfidence::ManualExact,
evidence: std::vec!["fixture".to_string()],
},
};
}
fn account(role: &str, account_key: &str) -> serde_json::Value {
return serde_json::json!({"role": role, "accountKey": account_key});
}
#[test]
fn loader_write_variants_create_bounded_code_audits() {
let materializer = crate::ComplianceAuditMaterializer;
for (surface, target_role, authority_role) in [
(super::BPF_LOADER_DEPRECATED_SURFACE, "program_account", "missing"),
(super::BPF_LOADER_SURFACE, "program_account", "missing"),
(super::BPF_LOADER_UPGRADEABLE_SURFACE, "buffer_account", "buffer_authority"),
(super::LOADER_V4_SURFACE, "program_account", "authority"),
] {
let observation = observation(
surface,
"write",
false,
serde_json::json!([
account(target_role, "target111"),
account(authority_role, "authority111"),
]),
serde_json::json!({
"offset": 7,
"byteLength": 32,
"bytesSha256": "abc",
"bytesPrefixHex": "0102",
}),
);
let result = crate::EventMaterializer::materialize(&materializer, &observation);
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Inserted);
assert_eq!(result.outputs[0].family, crate::MaterializedEventFamily::ComplianceAudit);
assert_eq!(result.outputs[0].payload_json["projection"]["targetAccount"], "target111");
assert_eq!(result.outputs[0].payload_json["projection"]["bytesSha256"], "abc");
assert_eq!(
result.outputs[0].payload_json["projection"]["codeBytesMaterialized"],
false
);
}
}
#[test]
fn loader_v4_copy_preserves_source_and_bounded_ranges() {
let materializer = crate::ComplianceAuditMaterializer;
let observation = observation(
super::LOADER_V4_SURFACE,
"copy",
false,
serde_json::json!([
account("program_account", "target111"),
account("authority", "authority111"),
account("source_program_account", "source111"),
]),
serde_json::json!({"destinationOffset": 8, "sourceOffset": 4, "length": 16}),
);
let result = crate::EventMaterializer::materialize(&materializer, &observation);
assert_eq!(
result.outputs[0].output_key,
"program_loader_code:solana_native_loader_v4:copy:0"
);
assert_eq!(
result.outputs[0].payload_json["projection"]["sourceProgramAccount"],
"source111"
);
assert_eq!(result.outputs[0].payload_json["projection"]["byteLength"], 16);
}
#[test]
fn compute_budget_profile_is_emitted_only_by_profile_emitter() {
let materializer = crate::ComplianceAuditMaterializer;
let emitter = observation(
super::COMPUTE_BUDGET_SURFACE,
"set_compute_unit_limit",
false,
serde_json::json!([]),
serde_json::json!({
"transactionProfile": {
"currentInstructionIsProfileEmitter": true,
"profileEmitterInstructionPath": "0",
"computeBudgetInstructionCount": 2,
"effectiveValues": {
"set_compute_unit_limit": {
"value": 200000,
"sourceInstructionPath": "0"
}
}
}
}),
);
let emitted = crate::EventMaterializer::materialize(&materializer, &emitter);
assert_eq!(emitted.status, crate::MaterializerOutcomeStatus::Inserted);
assert_eq!(emitted.outputs[0].output_key, "transaction_compute_budget_profile:0");
assert_eq!(emitted.outputs[0].family, crate::MaterializedEventFamily::ComplianceAudit);
assert_eq!(emitted.outputs[0].payload_json["profile"]["computeBudgetInstructionCount"], 2);
let non_emitter = observation(
super::COMPUTE_BUDGET_SURFACE,
"set_compute_unit_price",
false,
serde_json::json!([]),
serde_json::json!({
"transactionProfile": {
"currentInstructionIsProfileEmitter": false,
"profileEmitterInstructionPath": "0"
}
}),
);
let ignored = crate::EventMaterializer::materialize(&materializer, &non_emitter);
assert_eq!(ignored.status, crate::MaterializerOutcomeStatus::Ignored);
}
#[test]
fn failed_compute_budget_profile_remains_audit_materializable() {
let materializer = crate::ComplianceAuditMaterializer;
let observation = observation(
super::COMPUTE_BUDGET_SURFACE,
"set_compute_unit_price",
true,
serde_json::json!([]),
serde_json::json!({
"transactionProfile": {
"currentInstructionIsProfileEmitter": true,
"profileEmitterInstructionPath": "0",
"computeBudgetInstructionCount": 1
}
}),
);
let result = crate::EventMaterializer::materialize(&materializer, &observation);
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Inserted);
assert_eq!(result.outputs[0].payload_json["transactionSucceeded"], false);
}
#[test]
fn failed_code_mutation_is_refused() {
let materializer = crate::ComplianceAuditMaterializer;
let observation = observation(
super::LOADER_V4_SURFACE,
"write",
true,
serde_json::json!([account("program_account", "target111")]),
serde_json::json!({"offset": 0, "byteLength": 1, "bytesSha256": "abc"}),
);
let result = crate::EventMaterializer::materialize(&materializer, &observation);
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Refused);
assert!(result.outputs.is_empty());
}
#[test]
fn unrelated_audit_observation_is_ignored() {
let materializer = crate::ComplianceAuditMaterializer;
let observation = observation(
"other_surface",
"write",
false,
serde_json::json!([]),
serde_json::json!({}),
);
let result = crate::EventMaterializer::materialize(&materializer, &observation);
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Ignored);
}
#[test]
fn output_serialization_is_deterministic() {
let materializer = crate::ComplianceAuditMaterializer;
let observation = observation(
super::BPF_LOADER_UPGRADEABLE_SURFACE,
"write",
false,
serde_json::json!([
account("buffer_account", "buffer111"),
account("buffer_authority", "authority111"),
]),
serde_json::json!({
"offset": 0,
"byteLength": 2,
"bytesSha256": "abc",
"bytesPrefixHex": "0102",
}),
);
let first = crate::EventMaterializer::materialize(&materializer, &observation);
let second = crate::EventMaterializer::materialize(&materializer, &observation);
let first_json = match serde_json::to_string(&first) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
panic!("first compliance serialization failed: {error}")
},
};
let second_json = match serde_json::to_string(&second) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
panic!("second compliance serialization failed: {error}")
},
};
assert_eq!(first_json, second_json);
}
}

View File

@@ -0,0 +1,7 @@
// file: kb-lib/src/materializer/compliance/constants.rs
// version: 1
//! Local constants for the native compliance audit materializer.
/// Canonical tracing target for this component.
pub(crate) const TRACING_TARGET: &str = "kb-lib.materializer.compliance";

View File

@@ -1,6 +1,12 @@
// file: kb-lib/src/materializer/lifecycle.rs
// version: 1
// version: 2
//! `lifecycle` materializer family.
mod constants;
pub mod core;
pub(crate) use crate::materializer::lifecycle::constants::TRACING_TARGET as MATERIALIZER_LIFECYCLE_TRACING_TARGET;
/// Stable native lifecycle materializer.
pub use crate::materializer::lifecycle::core::LifecycleMaterializer;

View File

@@ -0,0 +1,7 @@
// file: kb-lib/src/materializer/lifecycle/constants.rs
// version: 1
//! Local constants for the native lifecycle materializer.
/// Canonical tracing target for this component.
pub(crate) const TRACING_TARGET: &str = "kb-lib.materializer.lifecycle";

View File

@@ -1,10 +1,960 @@
// file: kb-lib/src/materializer/lifecycle/core.rs
// version: 1
// version: 14
//! Migration boundary for legacy crate `kb_materializer_lifecycle`.
//! Stable lifecycle projections derived from exact decoded observations.
/// Legacy crate name retained for migration and compatibility tracking.
pub const LEGACY_CRATE: &str = "kb_materializer_lifecycle";
const ACCEPTED_FAMILIES: &[crate::EventFamily] = &[
crate::EventFamily::Lifecycle,
crate::EventFamily::Admin,
crate::EventFamily::Audit,
];
const ADDRESS_LOOKUP_TABLE_SURFACE: &str = "solana_native_address_lookup_table";
const ADDRESS_LOOKUP_TABLE_ENTRIES: &[&str] = &[
"create_lookup_table",
"freeze_lookup_table",
"extend_lookup_table",
"deactivate_lookup_table",
"close_lookup_table",
];
const BPF_LOADER_DEPRECATED_SURFACE: &str = "solana_native_bpf_loader_deprecated";
const BPF_LOADER_SURFACE: &str = "solana_native_bpf_loader";
const BPF_LOADER_UPGRADEABLE_SURFACE: &str = "solana_native_bpf_loader_upgradeable";
const LOADER_V4_SURFACE: &str = "solana_native_loader_v4";
const FEATURE_SURFACE: &str = "solana_native_feature";
const FEATURE_ENTRIES: &[&str] = &["revoke_pending_activation"];
const IMMUTABLE_LOADER_ENTRIES: &[&str] = &["finalize"];
const UPGRADEABLE_LOADER_ENTRIES: &[&str] = &[
"initialize_buffer",
"deploy_with_max_data_len",
"upgrade",
"close",
"extend_program",
];
const LOADER_V4_ENTRIES: &[&str] = &["set_program_length", "deploy", "retract", "finalize"];
const SLASHING_SURFACE: &str = "solana_native_slashing";
const SLASHING_ENTRIES: &[&str] = &["close_violation_report", "duplicate_block_proof"];
const SYSTEM_SURFACE: &str = "solana_native_system";
const SYSTEM_ACCOUNT_ENTRIES: &[&str] = &[
"create_account",
"create_account_with_seed",
"allocate",
"allocate_with_seed",
"create_account_allow_prefund",
];
const SYSTEM_NONCE_ENTRIES: &[&str] = &[
"advance_nonce_account",
"withdraw_nonce_account",
"initialize_nonce_account",
"authorize_nonce_account",
"upgrade_nonce_account",
];
const ZK_ELGAMAL_PROOF_SURFACE: &str = "solana_native_zk_elgamal_proof";
const ZK_ELGAMAL_CLOSE_ENTRIES: &[&str] = &["close_context_state"];
const ZK_ELGAMAL_VERIFY_ENTRIES: &[&str] = &[
"verify_zero_ciphertext",
"verify_ciphertext_ciphertext_equality",
"verify_ciphertext_commitment_equality",
"verify_pubkey_validity",
"verify_percentage_with_cap",
"verify_batched_range_proof_u64",
"verify_batched_range_proof_u128",
"verify_batched_range_proof_u256",
"verify_grouped_ciphertext_2_handles_validity",
"verify_batched_grouped_ciphertext_2_handles_validity",
"verify_grouped_ciphertext_3_handles_validity",
"verify_batched_grouped_ciphertext_3_handles_validity",
];
/// Current porting status.
pub const MIGRATION_STATUS: &str = "source-preserved-pending-port";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum ProjectionKind {
AddressLookupTable,
FeatureGate,
ProgramLoader(&'static str),
SlashingViolationReportInitialization,
SlashingViolationReportClosure,
SystemAccountLifecycle,
DurableNonceAccount,
ZkProofContextInitialization,
ZkProofContextClosure,
}
impl ProjectionKind {
fn domain(self) -> &'static str {
return match self {
Self::AddressLookupTable => "address_lookup_table",
Self::FeatureGate => "feature_gate",
Self::ProgramLoader(_) => "program_loader",
Self::SlashingViolationReportInitialization | Self::SlashingViolationReportClosure => {
"slashing_violation_report"
},
Self::SystemAccountLifecycle => "system_account",
Self::DurableNonceAccount => "durable_nonce_account",
Self::ZkProofContextInitialization | Self::ZkProofContextClosure => "zk_proof_context",
};
}
fn loader_surface(self) -> std::option::Option<&'static str> {
return match self {
Self::ProgramLoader(surface) => std::option::Option::Some(surface),
_ => std::option::Option::None,
};
}
fn output_key(self, surface_code: &str, operation: &str) -> std::string::String {
return match self {
Self::AddressLookupTable => format!("address_lookup_table:{operation}:0"),
Self::FeatureGate => format!("feature_gate:{operation}:0"),
Self::ProgramLoader(_) => {
format!("program_loader:{surface_code}:{operation}:0")
},
Self::SlashingViolationReportInitialization | Self::SlashingViolationReportClosure => {
format!("slashing_violation_report:{operation}:0")
},
Self::SystemAccountLifecycle => format!("system_account:{operation}:0"),
Self::DurableNonceAccount => format!("durable_nonce_account:{operation}:0"),
Self::ZkProofContextInitialization | Self::ZkProofContextClosure => {
format!("zk_proof_context:{operation}:0")
},
};
}
}
/// Stable lifecycle materializer for supported native lifecycle projections.
#[derive(Clone, Debug, Default)]
pub struct LifecycleMaterializer;
impl crate::Materializer for crate::LifecycleMaterializer {
fn materializer_name(&self) -> &'static str {
return "kb_materializer_lifecycle";
}
fn materializer_version(&self) -> &'static str {
return env!("CARGO_PKG_VERSION");
}
fn accepts_event(&self, event: &crate::DecodedProtocolEvent) -> bool {
return family_is_accepted(event.event_family)
&& static_projection(event.surface_code.0.as_str(), event.event_name.0.as_str())
.is_some();
}
fn materialize_event(
&self,
event: &crate::DecodedProtocolEvent,
) -> kb_core::Result<std::vec::Vec<crate::MaterializedEvent>> {
if !crate::Materializer::accepts_event(self, event) {
return std::result::Result::Ok(std::vec::Vec::new());
}
return std::result::Result::Ok(std::vec::Vec::new());
}
}
impl crate::EventMaterializer for crate::LifecycleMaterializer {
fn identity(&self) -> crate::MaterializerIdentity {
return crate::MaterializerIdentity {
name: "solana_native_lifecycle".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
};
}
fn accepted_families(&self) -> &'static [crate::EventFamily] {
return ACCEPTED_FAMILIES;
}
fn accepts_observation(&self, observation: &crate::DecodedObservation) -> bool {
return family_is_accepted(observation.event.event_family)
&& observation_projection(observation).is_some();
}
fn transaction_policy(
&self,
_family: crate::EventFamily,
) -> crate::MaterializationTransactionPolicy {
return crate::MaterializationTransactionPolicy::SuccessfulCommittedOnly;
}
fn materialize(
&self,
observation: &crate::DecodedObservation,
) -> crate::MaterializerExecutionResult {
let projection = match observation_projection(observation) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
tracing::debug!(
target: crate::MATERIALIZER_LIFECYCLE_TRACING_TARGET,
action = "materialize_lifecycle",
surface_code = %observation.event.surface_code.0.as_str(),
entry_code = %observation.event.event_name.0.as_str(),
accepted = false,
"ignore observation outside supported native lifecycle projections"
);
return crate::MaterializerExecutionResult::ignored();
},
};
if observation.transaction_failed || !observation.observation_committed {
tracing::debug!(
target: crate::MATERIALIZER_LIFECYCLE_TRACING_TARGET,
action = "materialize_lifecycle",
surface_code = %observation.event.surface_code.0.as_str(),
entry_code = %observation.event.event_name.0.as_str(),
accepted = true,
committed = observation.observation_committed,
transaction_failed = observation.transaction_failed,
"refuse uncommitted native lifecycle observation"
);
return crate::MaterializerExecutionResult::refused(
"failed_transaction_lifecycle_refused",
"failed or uncommitted native lifecycle observations cannot create lifecycle outputs",
);
}
let accounts = observation
.payload_json
.get("accounts")
.cloned()
.unwrap_or(serde_json::Value::Null);
let parameters = observation
.payload_json
.get("parameters")
.cloned()
.unwrap_or(serde_json::Value::Null);
let operation = observation.event.event_name.0.clone();
let output_key =
projection.output_key(observation.event.surface_code.0.as_str(), operation.as_str());
let domain = projection.domain();
let loader_surface = projection.loader_surface();
let projection_details =
projection_details(projection, &accounts, &parameters, operation.as_str());
tracing::debug!(
target: crate::MATERIALIZER_LIFECYCLE_TRACING_TARGET,
action = "materialize_lifecycle",
surface_code = %observation.event.surface_code.0.as_str(),
entry_code = %observation.event.event_name.0.as_str(),
projection_domain = domain,
accepted = true,
committed = true,
output_count = 1_usize,
"materialize committed native lifecycle observation"
);
let output = crate::MaterializedOutput {
output_key,
family: crate::MaterializedEventFamily::Lifecycle,
payload_json: serde_json::json!({
"projectionVersion": 1,
"projectionSemantics": "committed_instruction_lifecycle_event",
"domain": domain,
"loaderSurface": loader_surface,
"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(),
"transactionSucceeded": true,
"accounts": accounts,
"parameters": parameters,
"projection": projection_details,
}),
};
return crate::MaterializerExecutionResult {
status: crate::MaterializerOutcomeStatus::Inserted,
outputs: std::vec![output],
diagnostics: std::vec::Vec::new(),
};
}
}
fn family_is_accepted(family: crate::EventFamily) -> bool {
return ACCEPTED_FAMILIES.iter().any(|accepted| return *accepted == family);
}
fn observation_projection(
observation: &crate::DecodedObservation,
) -> std::option::Option<ProjectionKind> {
let surface_code = observation.event.surface_code.0.as_str();
let entry_code = observation.event.event_name.0.as_str();
let static_projection = static_projection(surface_code, entry_code);
if static_projection.is_some() {
return static_projection;
}
if surface_code == ZK_ELGAMAL_PROOF_SURFACE
&& contains(ZK_ELGAMAL_VERIFY_ENTRIES, entry_code)
&& observation
.payload_json
.get("parameters")
.and_then(|parameters| return parameters.get("contextStateRequested"))
.and_then(serde_json::Value::as_bool)
== std::option::Option::Some(true)
{
return std::option::Option::Some(ProjectionKind::ZkProofContextInitialization);
}
return std::option::Option::None;
}
fn static_projection(surface_code: &str, entry_code: &str) -> std::option::Option<ProjectionKind> {
if surface_code == FEATURE_SURFACE && contains(FEATURE_ENTRIES, entry_code) {
return std::option::Option::Some(ProjectionKind::FeatureGate);
}
if surface_code == ADDRESS_LOOKUP_TABLE_SURFACE
&& contains(ADDRESS_LOOKUP_TABLE_ENTRIES, entry_code)
{
return std::option::Option::Some(ProjectionKind::AddressLookupTable);
}
if (surface_code == BPF_LOADER_DEPRECATED_SURFACE || surface_code == BPF_LOADER_SURFACE)
&& contains(IMMUTABLE_LOADER_ENTRIES, entry_code)
{
let loader_surface = if surface_code == BPF_LOADER_DEPRECATED_SURFACE {
BPF_LOADER_DEPRECATED_SURFACE
} else {
BPF_LOADER_SURFACE
};
return std::option::Option::Some(ProjectionKind::ProgramLoader(loader_surface));
}
if surface_code == BPF_LOADER_UPGRADEABLE_SURFACE
&& contains(UPGRADEABLE_LOADER_ENTRIES, entry_code)
{
return std::option::Option::Some(ProjectionKind::ProgramLoader(
BPF_LOADER_UPGRADEABLE_SURFACE,
));
}
if surface_code == LOADER_V4_SURFACE && contains(LOADER_V4_ENTRIES, entry_code) {
return std::option::Option::Some(ProjectionKind::ProgramLoader(LOADER_V4_SURFACE));
}
if surface_code == SLASHING_SURFACE && contains(SLASHING_ENTRIES, entry_code) {
return if entry_code == "duplicate_block_proof" {
std::option::Option::Some(ProjectionKind::SlashingViolationReportInitialization)
} else {
std::option::Option::Some(ProjectionKind::SlashingViolationReportClosure)
};
}
if surface_code == SYSTEM_SURFACE && contains(SYSTEM_ACCOUNT_ENTRIES, entry_code) {
return std::option::Option::Some(ProjectionKind::SystemAccountLifecycle);
}
if surface_code == SYSTEM_SURFACE && contains(SYSTEM_NONCE_ENTRIES, entry_code) {
return std::option::Option::Some(ProjectionKind::DurableNonceAccount);
}
if surface_code == ZK_ELGAMAL_PROOF_SURFACE && contains(ZK_ELGAMAL_CLOSE_ENTRIES, entry_code) {
return std::option::Option::Some(ProjectionKind::ZkProofContextClosure);
}
return std::option::Option::None;
}
fn projection_details(
projection: ProjectionKind,
accounts: &serde_json::Value,
parameters: &serde_json::Value,
operation: &str,
) -> serde_json::Value {
return match projection {
ProjectionKind::SlashingViolationReportInitialization => {
serde_json::json!({
"targetAccount": account_key_for_role(accounts, "violation_report"),
"proofAccount": account_key_for_role(accounts, "proof_account"),
"violationSlot": parameters.get("violationSlot").cloned().unwrap_or(serde_json::Value::Null),
"nodePubkey": parameters.get("nodePubkey").cloned().unwrap_or(serde_json::Value::Null),
"reporter": parameters.get("reporter").cloned().unwrap_or(serde_json::Value::Null),
"lamportDestination": parameters.get("destination").cloned().unwrap_or(serde_json::Value::Null),
"stateTransition": "pda_assigned_allocated_and_data_stored_after_successful_duplicate_block_proof_acceptance",
"proofMaterialized": false,
"penaltyAppliedByProgram": false,
"enforcementSemantics": "violation_report_only_external_consensus_enforcement",
})
},
ProjectionKind::SlashingViolationReportClosure => serde_json::json!({
"targetAccount": account_key_for_role(accounts, "violation_report"),
"lamportDestination": account_key_for_role(accounts, "destination"),
"stateTransition": "closed_after_minimum_retention_lamports_reclaimed_owner_reset_to_system_program",
"minimumRetentionEpochs": 3,
"transactionFinalAccountStateCaptured": false,
}),
ProjectionKind::SystemAccountLifecycle => serde_json::json!({
"targetAccount": first_account_key_for_roles(
accounts,
&["new_account", "allocated_account", "derived_account"],
),
"fundingAccount": account_key_for_role(accounts, "funding_account"),
"baseAccount": account_key_for_role(accounts, "base_account"),
"requestedLamports": parameters.get("lamports").cloned().unwrap_or(serde_json::Value::Null),
"requestedSpace": parameters.get("space").cloned().unwrap_or(serde_json::Value::Null),
"requestedOwner": parameters.get("owner").cloned().unwrap_or(serde_json::Value::Null),
"seed": parameters.get("seed").cloned().unwrap_or(serde_json::Value::Null),
"stateTransition": system_account_transition(operation),
"lamportTransferRepresentedByCoreBalanceChanges": operation == "create_account"
|| operation == "create_account_with_seed"
|| operation == "create_account_allow_prefund",
"transactionFinalAccountStateCaptured": false,
}),
ProjectionKind::DurableNonceAccount => serde_json::json!({
"targetAccount": account_key_for_role(accounts, "nonce_account"),
"authorityAccount": account_key_for_role(accounts, "nonce_authority"),
"lamportDestination": account_key_for_role(accounts, "recipient_account"),
"stateTransition": nonce_transition(operation),
"newAuthority": parameters.get("authority").cloned().unwrap_or(serde_json::Value::Null),
"withdrawLamports": parameters.get("lamports").cloned().unwrap_or(serde_json::Value::Null),
"closureSemantics": if operation == "withdraw_nonce_account" {
serde_json::Value::String("closes_when_entire_balance_is_withdrawn".to_string())
} else {
serde_json::Value::Null
},
"transactionFinalAccountStateCaptured": false,
}),
ProjectionKind::ZkProofContextInitialization => serde_json::json!({
"targetAccount": account_key_for_role(accounts, "proof_context_state"),
"contextStateAuthority": account_key_for_role(accounts, "context_state_authority"),
"proofType": parameters.get("proofType").cloned().unwrap_or(serde_json::Value::Null),
"stateTransition": "initialized_after_successful_runtime_verification",
"proofMaterialized": false,
}),
ProjectionKind::ZkProofContextClosure => serde_json::json!({
"targetAccount": account_key_for_role(accounts, "proof_context_state"),
"lamportDestination": account_key_for_role(accounts, "lamport_destination"),
"contextStateAuthority": account_key_for_role(accounts, "context_state_authority"),
"stateTransition": "closed_lamports_reclaimed_owner_reset_to_system_program",
"proofMaterialized": false,
}),
ProjectionKind::AddressLookupTable
| ProjectionKind::FeatureGate
| ProjectionKind::ProgramLoader(_) => serde_json::json!({
"stateTransition": operation,
}),
};
}
fn system_account_transition(operation: &str) -> &'static str {
return match operation {
"create_account" => "created_allocated_funded_and_assigned",
"create_account_with_seed" => "derived_account_created_allocated_funded_and_assigned",
"create_account_allow_prefund" => "prefunded_account_initialized_allocated_and_assigned",
"allocate" => "account_data_space_allocated",
"allocate_with_seed" => "derived_account_data_space_allocated",
_ => "unknown",
};
}
fn nonce_transition(operation: &str) -> &'static str {
return match operation {
"initialize_nonce_account" => "initialized",
"advance_nonce_account" => "nonce_advanced",
"authorize_nonce_account" => "authority_changed",
"upgrade_nonce_account" => "legacy_version_upgraded",
"withdraw_nonce_account" => "lamports_withdrawn_possible_closure",
_ => "unknown",
};
}
fn first_account_key_for_roles(accounts: &serde_json::Value, roles: &[&str]) -> serde_json::Value {
for role in roles {
let value = account_key_for_role(accounts, role);
if !value.is_null() {
return value;
}
}
return serde_json::Value::Null;
}
fn account_key_for_role(accounts: &serde_json::Value, role: &str) -> serde_json::Value {
let account_key = accounts.as_array().and_then(|values| {
return values.iter().find_map(|value| {
if value.get("role").and_then(serde_json::Value::as_str)
!= std::option::Option::Some(role)
{
return std::option::Option::None;
}
return value
.get("accountKey")
.and_then(serde_json::Value::as_str)
.map(|account_key| return account_key.to_string());
});
});
return match account_key {
std::option::Option::Some(value) => serde_json::Value::String(value),
std::option::Option::None => serde_json::Value::Null,
};
}
fn contains(entries: &[&str], entry_code: &str) -> bool {
return entries.iter().any(|entry| return *entry == entry_code);
}
#[cfg(test)]
mod tests {
fn observation_with_payload(
surface_code: &str,
entry_code: &str,
event_family: crate::EventFamily,
transaction_failed: bool,
accounts: serde_json::Value,
parameters: serde_json::Value,
) -> crate::DecodedObservation {
return crate::DecodedObservation {
event_key: format!("{entry_code}:0"),
event: crate::DecodedProtocolEvent {
signature: crate::Signature("signature".to_string()),
slot: crate::Slot(42),
instruction_path: crate::InstructionPath("0".to_string()),
program_id: crate::ProgramId("program-id".to_string()),
protocol_code: crate::ProtocolCode("solana_native".to_string()),
surface_code: crate::SurfaceCode(surface_code.to_string()),
event_code: crate::EventCode(format!("{surface_code}.{entry_code}")),
event_name: crate::EventName(entry_code.to_string()),
event_family,
source_kind: crate::EventSourceKind::Instruction,
confidence: crate::DecoderConfidence::ManualExact,
},
payload_json: serde_json::json!({
"accounts": accounts,
"parameters": parameters,
}),
transaction_failed,
transaction_error: if transaction_failed {
std::option::Option::Some(serde_json::json!({"InstructionError": [0, "Custom"]}))
} else {
std::option::Option::None
},
observation_committed: !transaction_failed,
proof: crate::DecoderProof {
kind: crate::DecoderProofKind::Manual,
confidence: crate::DecoderConfidence::ManualExact,
evidence: std::vec!["fixture".to_string()],
},
};
}
fn observation(
surface_code: &str,
entry_code: &str,
transaction_failed: bool,
) -> crate::DecodedObservation {
return observation_with_payload(
surface_code,
entry_code,
crate::EventFamily::Lifecycle,
transaction_failed,
serde_json::json!([{"role": "target", "accountKey": "account"}]),
serde_json::json!({"value": 41}),
);
}
fn nonce_observation(
entry_code: &str,
family: crate::EventFamily,
transaction_failed: bool,
) -> crate::DecodedObservation {
return observation_with_payload(
super::SYSTEM_SURFACE,
entry_code,
family,
transaction_failed,
serde_json::json!([
{"role": "nonce_account", "accountKey": "nonce111"},
{"role": "recipient_account", "accountKey": "recipient111"},
{"role": "nonce_authority", "accountKey": "authority111"}
]),
serde_json::json!({
"authority": "new-authority111",
"lamports": 55,
}),
);
}
fn slashing_observation(
entry_code: &str,
transaction_failed: bool,
) -> crate::DecodedObservation {
let (family, accounts) = if entry_code == "duplicate_block_proof" {
(
crate::EventFamily::Audit,
serde_json::json!([
{"role": "proof_account", "accountKey": "proof111"},
{"role": "violation_report", "accountKey": "report111"},
{"role": "instructions_sysvar", "accountKey": kb_program_ids::SYSVAR_INSTRUCTIONS_PROGRAM_ID},
{"role": "system_program", "accountKey": kb_program_ids::SYSTEM_PROGRAM_ID}
]),
)
} else {
(
crate::EventFamily::Lifecycle,
serde_json::json!([
{"role": "violation_report", "accountKey": "report111"},
{"role": "destination", "accountKey": "destination111"}
]),
)
};
return observation_with_payload(
super::SLASHING_SURFACE,
entry_code,
family,
transaction_failed,
accounts,
serde_json::json!({
"proofType": "duplicate_block",
"violationSlot": 42,
"nodePubkey": "node111",
"reporter": "reporter111",
"destination": "destination111",
"penaltyAppliedByProgram": false,
}),
);
}
fn zk_observation(
entry_code: &str,
context_state_requested: bool,
transaction_failed: bool,
) -> crate::DecodedObservation {
let accounts = if entry_code == "close_context_state" {
serde_json::json!([
{"role": "proof_context_state", "accountKey": "context111"},
{"role": "lamport_destination", "accountKey": "destination111"},
{"role": "context_state_authority", "accountKey": "authority111"}
])
} else if context_state_requested {
serde_json::json!([
{"role": "proof_context_state", "accountKey": "context111"},
{"role": "context_state_authority", "accountKey": "authority111"}
])
} else {
serde_json::json!([])
};
let family = if entry_code == "close_context_state" {
crate::EventFamily::Lifecycle
} else {
crate::EventFamily::Audit
};
return observation_with_payload(
super::ZK_ELGAMAL_PROOF_SURFACE,
entry_code,
family,
transaction_failed,
accounts,
serde_json::json!({
"proofType": entry_code,
"contextStateRequested": context_state_requested,
}),
);
}
#[test]
fn successful_alt_lifecycle_creates_one_stable_output() {
let materializer = crate::LifecycleMaterializer;
let result = crate::EventMaterializer::materialize(
&materializer,
&observation(super::ADDRESS_LOOKUP_TABLE_SURFACE, "create_lookup_table", false),
);
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Inserted);
assert_eq!(result.outputs.len(), 1);
assert_eq!(result.outputs[0].output_key, "address_lookup_table:create_lookup_table:0");
assert_eq!(result.outputs[0].payload_json["domain"], "address_lookup_table");
assert_eq!(result.outputs[0].payload_json["loaderSurface"], serde_json::Value::Null);
}
#[test]
fn successful_loader_lifecycle_creates_program_projection() {
let materializer = crate::LifecycleMaterializer;
let result = crate::EventMaterializer::materialize(
&materializer,
&observation(super::BPF_LOADER_UPGRADEABLE_SURFACE, "deploy_with_max_data_len", false),
);
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Inserted);
assert_eq!(
result.outputs[0].output_key,
"program_loader:solana_native_bpf_loader_upgradeable:deploy_with_max_data_len:0"
);
assert_eq!(result.outputs[0].payload_json["domain"], "program_loader");
assert_eq!(
result.outputs[0].payload_json["loaderSurface"],
super::BPF_LOADER_UPGRADEABLE_SURFACE
);
}
#[test]
fn successful_feature_revoke_creates_feature_gate_projection() {
let materializer = crate::LifecycleMaterializer;
let result = crate::EventMaterializer::materialize(
&materializer,
&observation(super::FEATURE_SURFACE, "revoke_pending_activation", false),
);
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Inserted);
assert_eq!(result.outputs[0].output_key, "feature_gate:revoke_pending_activation:0");
assert_eq!(result.outputs[0].payload_json["domain"], "feature_gate");
}
#[test]
fn every_committed_system_account_operation_creates_a_lifecycle_projection() {
let materializer = crate::LifecycleMaterializer;
for (operation, role, transition) in [
("create_account", "new_account", "created_allocated_funded_and_assigned"),
(
"create_account_with_seed",
"new_account",
"derived_account_created_allocated_funded_and_assigned",
),
("allocate", "allocated_account", "account_data_space_allocated"),
("allocate_with_seed", "derived_account", "derived_account_data_space_allocated"),
(
"create_account_allow_prefund",
"new_account",
"prefunded_account_initialized_allocated_and_assigned",
),
] {
let observation = observation_with_payload(
super::SYSTEM_SURFACE,
operation,
crate::EventFamily::Lifecycle,
false,
serde_json::json!([
{"role": role, "accountKey": "target111"},
{"role": "funding_account", "accountKey": "funding111"},
{"role": "base_account", "accountKey": "base111"},
]),
serde_json::json!({
"lamports": 55,
"space": 64,
"owner": "owner111",
"seed": "seed",
}),
);
let result = crate::EventMaterializer::materialize(&materializer, &observation);
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Inserted);
assert_eq!(result.outputs[0].output_key, format!("system_account:{operation}:0"));
assert_eq!(result.outputs[0].payload_json["projection"]["targetAccount"], "target111");
assert_eq!(result.outputs[0].payload_json["projection"]["stateTransition"], transition);
}
}
#[test]
fn system_account_creation_does_not_duplicate_balance_changes() {
let materializer = crate::LifecycleMaterializer;
let observation = observation_with_payload(
super::SYSTEM_SURFACE,
"create_account",
crate::EventFamily::Lifecycle,
false,
serde_json::json!([
{"role": "funding_account", "accountKey": "funding111"},
{"role": "new_account", "accountKey": "target111"},
]),
serde_json::json!({"lamports": 55, "space": 64, "owner": "owner111"}),
);
let result = crate::EventMaterializer::materialize(&materializer, &observation);
assert_eq!(result.outputs[0].payload_json["projection"]["requestedLamports"], 55);
assert_eq!(
result.outputs[0].payload_json["projection"]["lamportTransferRepresentedByCoreBalanceChanges"],
true
);
assert!(result.outputs[0].payload_json["projection"].get("balanceChanges").is_none());
}
#[test]
fn every_committed_system_nonce_operation_creates_a_lifecycle_projection() {
let materializer = crate::LifecycleMaterializer;
for (entry_code, family, transition) in [
("initialize_nonce_account", crate::EventFamily::Lifecycle, "initialized"),
("advance_nonce_account", crate::EventFamily::Lifecycle, "nonce_advanced"),
("authorize_nonce_account", crate::EventFamily::Admin, "authority_changed"),
(
"upgrade_nonce_account",
crate::EventFamily::Lifecycle,
"legacy_version_upgraded",
),
(
"withdraw_nonce_account",
crate::EventFamily::Lifecycle,
"lamports_withdrawn_possible_closure",
),
] {
let observation = nonce_observation(entry_code, family, false);
assert!(crate::EventMaterializer::accepts_observation(&materializer, &observation,));
let result = crate::EventMaterializer::materialize(&materializer, &observation);
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Inserted);
assert_eq!(result.outputs[0].payload_json["domain"], "durable_nonce_account");
assert_eq!(result.outputs[0].payload_json["projection"]["targetAccount"], "nonce111");
assert_eq!(
result.outputs[0].payload_json["projection"]["authorityAccount"],
"authority111"
);
assert_eq!(result.outputs[0].payload_json["projection"]["stateTransition"], transition);
}
}
#[test]
fn nonce_withdrawal_preserves_conditional_closure_semantics() {
let materializer = crate::LifecycleMaterializer;
let observation =
nonce_observation("withdraw_nonce_account", crate::EventFamily::Lifecycle, false);
let result = crate::EventMaterializer::materialize(&materializer, &observation);
assert_eq!(result.outputs[0].payload_json["projection"]["withdrawLamports"], 55);
assert_eq!(
result.outputs[0].payload_json["projection"]["lamportDestination"],
"recipient111"
);
assert_eq!(
result.outputs[0].payload_json["projection"]["closureSemantics"],
"closes_when_entire_balance_is_withdrawn"
);
assert_eq!(
result.outputs[0].payload_json["projection"]["transactionFinalAccountStateCaptured"]
.as_bool(),
std::option::Option::Some(false)
);
}
#[test]
fn zk_context_is_materialized_only_when_requested() {
let materializer = crate::LifecycleMaterializer;
let without_context = zk_observation("verify_zero_ciphertext", false, false);
assert!(!crate::EventMaterializer::accepts_observation(&materializer, &without_context,));
let with_context = zk_observation("verify_zero_ciphertext", true, false);
assert!(crate::EventMaterializer::accepts_observation(&materializer, &with_context,));
let result = crate::EventMaterializer::materialize(&materializer, &with_context);
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Inserted);
assert_eq!(result.outputs[0].payload_json["domain"], "zk_proof_context");
assert_eq!(result.outputs[0].payload_json["projection"]["targetAccount"], "context111");
assert_eq!(
result.outputs[0].payload_json["projection"]["stateTransition"],
"initialized_after_successful_runtime_verification"
);
assert_eq!(
result.outputs[0].payload_json["projection"]["proofMaterialized"].as_bool(),
std::option::Option::Some(false)
);
}
#[test]
fn zk_context_close_creates_a_stable_lifecycle_projection() {
let materializer = crate::LifecycleMaterializer;
let observation = zk_observation("close_context_state", false, false);
let result = crate::EventMaterializer::materialize(&materializer, &observation);
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Inserted);
assert_eq!(result.outputs[0].output_key, "zk_proof_context:close_context_state:0");
assert_eq!(
result.outputs[0].payload_json["projection"]["lamportDestination"],
"destination111"
);
assert_eq!(
result.outputs[0].payload_json["projection"]["stateTransition"],
"closed_lamports_reclaimed_owner_reset_to_system_program"
);
}
#[test]
fn successful_slashing_proof_initializes_violation_report_projection() {
let materializer = crate::LifecycleMaterializer;
let observation = slashing_observation("duplicate_block_proof", false);
assert!(crate::EventMaterializer::accepts_observation(&materializer, &observation,));
let result = crate::EventMaterializer::materialize(&materializer, &observation);
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Inserted);
assert_eq!(
result.outputs[0].output_key,
"slashing_violation_report:duplicate_block_proof:0"
);
assert_eq!(result.outputs[0].payload_json["domain"], "slashing_violation_report");
assert_eq!(result.outputs[0].payload_json["projection"]["targetAccount"], "report111");
assert_eq!(result.outputs[0].payload_json["projection"]["proofMaterialized"], false);
assert_eq!(result.outputs[0].payload_json["projection"]["penaltyAppliedByProgram"], false);
}
#[test]
fn successful_slashing_close_creates_stable_closure_projection() {
let materializer = crate::LifecycleMaterializer;
let observation = slashing_observation("close_violation_report", false);
let result = crate::EventMaterializer::materialize(&materializer, &observation);
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Inserted);
assert_eq!(
result.outputs[0].payload_json["projection"]["stateTransition"],
"closed_after_minimum_retention_lamports_reclaimed_owner_reset_to_system_program"
);
assert_eq!(result.outputs[0].payload_json["projection"]["minimumRetentionEpochs"], 3);
assert_eq!(
result.outputs[0].payload_json["projection"]["lamportDestination"],
"destination111"
);
}
#[test]
fn historical_zk_token_proof_is_not_materialized() {
let materializer = crate::LifecycleMaterializer;
let observation = observation_with_payload(
"solana_native_zk_token_proof",
"close_context_state",
crate::EventFamily::Audit,
false,
serde_json::json!([]),
serde_json::json!({}),
);
assert!(!crate::EventMaterializer::accepts_observation(&materializer, &observation,));
let result = crate::EventMaterializer::materialize(&materializer, &observation);
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Ignored);
}
#[test]
fn loader_write_and_admin_events_are_not_lifecycle_projections() {
let materializer = crate::LifecycleMaterializer;
for entry_code in ["write", "set_authority", "transfer_authority"] {
let observation = observation(super::BPF_LOADER_UPGRADEABLE_SURFACE, entry_code, false);
assert!(!crate::EventMaterializer::accepts_observation(&materializer, &observation,));
}
}
#[test]
fn unrelated_lifecycle_is_ignored() {
let materializer = crate::LifecycleMaterializer;
let result = crate::EventMaterializer::materialize(
&materializer,
&observation(super::SYSTEM_SURFACE, "transfer", false),
);
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Ignored);
}
#[test]
fn ata_lifecycle_is_owned_by_token_accounts_materializer_only() {
let mut observation = observation("spl_associated_token_account", "create", false);
observation.event.program_id =
crate::ProgramId(kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID.to_string());
assert!(!crate::EventMaterializer::accepts_observation(
&crate::LifecycleMaterializer,
&observation,
));
let result =
crate::EventMaterializer::materialize(&crate::LifecycleMaterializer, &observation);
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Ignored);
assert!(result.outputs.is_empty());
}
#[test]
fn failed_native_lifecycle_is_refused() {
let materializer = crate::LifecycleMaterializer;
let observations = [
observation(super::ADDRESS_LOOKUP_TABLE_SURFACE, "close_lookup_table", true),
observation(super::LOADER_V4_SURFACE, "deploy", true),
nonce_observation("authorize_nonce_account", crate::EventFamily::Admin, true),
zk_observation("verify_zero_ciphertext", true, true),
slashing_observation("duplicate_block_proof", true),
];
for observation in observations {
let policy = crate::validate_materialization_policy(&materializer, &observation);
assert!(policy.is_err());
let direct = crate::EventMaterializer::materialize(&materializer, &observation);
assert_eq!(direct.status, crate::MaterializerOutcomeStatus::Refused);
}
}
#[test]
fn output_serialization_is_deterministic() {
let materializer = crate::LifecycleMaterializer;
let observation = zk_observation("verify_batched_range_proof_u64", true, false);
let first = crate::EventMaterializer::materialize(&materializer, &observation);
let second = crate::EventMaterializer::materialize(&materializer, &observation);
let first_json = match serde_json::to_string(&first) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
panic!("first lifecycle serialization failed: {error}")
},
};
let second_json = match serde_json::to_string(&second) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
panic!("second lifecycle serialization failed: {error}")
},
};
assert_eq!(first_json, second_json);
}
}

View File

@@ -1,6 +1,12 @@
// file: kb-lib/src/materializer/staking.rs
// version: 1
// version: 2
//! `staking` materializer family.
mod constants;
pub mod core;
pub(crate) use crate::materializer::staking::constants::TRACING_TARGET as MATERIALIZER_STAKING_TRACING_TARGET;
/// Stable native stake and vote materializer.
pub use crate::materializer::staking::core::StakingMaterializer;

View File

@@ -0,0 +1,7 @@
// file: kb-lib/src/materializer/staking/constants.rs
// version: 1
//! Local constants for the native stake and vote materializer.
/// Canonical tracing target for this component.
pub(crate) const TRACING_TARGET: &str = "kb-lib.materializer.staking";

View File

@@ -1,10 +1,867 @@
// file: kb-lib/src/materializer/staking/core.rs
// version: 1
// version: 7
//! Migration boundary for legacy crate `kb_materializer_staking`.
//! Instruction-level staking projections for native Stake and Vote observations.
/// Legacy crate name retained for migration and compatibility tracking.
pub const LEGACY_CRATE: &str = "kb_materializer_staking";
const ACCEPTED_FAMILIES: &[crate::EventFamily] = &[
crate::EventFamily::Lifecycle,
crate::EventFamily::Admin,
crate::EventFamily::Staking,
crate::EventFamily::Reward,
];
const STAKE_SURFACE: &str = "solana_native_stake";
const VOTE_SURFACE: &str = "solana_native_vote";
const STAKE_ACCOUNT_ENTRIES: &[&str] = &[
"initialize",
"initialize_checked",
"delegate_stake",
"deactivate",
"deactivate_delinquent",
];
const STAKE_VALUE_ENTRIES: &[&str] = &["split", "withdraw", "merge", "move_stake", "move_lamports"];
const STAKE_AUTHORITY_ENTRIES: &[&str] = &[
"authorize",
"authorize_with_seed",
"authorize_checked",
"authorize_checked_with_seed",
];
const STAKE_LOCKUP_ENTRIES: &[&str] = &["set_lockup", "set_lockup_checked"];
const VOTE_ACCOUNT_ENTRIES: &[&str] = &["initialize_account", "initialize_account_v2"];
const VOTE_AUTHORITY_ENTRIES: &[&str] = &[
"authorize",
"authorize_checked",
"authorize_with_seed",
"authorize_checked_with_seed",
];
const VOTE_ADMIN_ENTRIES: &[&str] = &[
"update_validator_identity",
"update_commission",
"update_commission_collector",
"update_commission_bps",
];
const VOTE_STATE_ENTRIES: &[&str] = &[
"vote",
"vote_switch",
"update_vote_state",
"update_vote_state_switch",
"compact_update_vote_state",
"compact_update_vote_state_switch",
"tower_sync",
"tower_sync_switch",
];
const VOTE_VALUE_ENTRIES: &[&str] = &["withdraw"];
const VOTE_REWARD_ENTRIES: &[&str] = &["deposit_delegator_rewards"];
/// Current porting status.
pub const MIGRATION_STATUS: &str = "source-preserved-pending-port";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum ProjectionKind {
StakeAccount,
StakeValue,
StakeAuthority,
StakeLockup,
VoteAccount,
VoteAuthority,
VoteAdmin,
VoteState,
VoteValue,
VoteReward,
}
impl ProjectionKind {
fn domain(self) -> &'static str {
return match self {
Self::StakeAccount => "stake_account",
Self::StakeValue => "stake_value",
Self::StakeAuthority => "stake_authority",
Self::StakeLockup => "stake_lockup",
Self::VoteAccount => "vote_account",
Self::VoteAuthority => "vote_authority",
Self::VoteAdmin => "vote_admin",
Self::VoteState => "vote_state",
Self::VoteValue => "vote_value",
Self::VoteReward => "vote_reward",
};
}
fn family(self) -> crate::MaterializedEventFamily {
return match self {
Self::VoteReward => crate::MaterializedEventFamily::Reward,
_ => crate::MaterializedEventFamily::Staking,
};
}
fn output_key(self, operation: &str) -> std::string::String {
return format!("{}:{operation}:0", self.domain());
}
}
/// Stable staking materializer for committed native Stake and Vote observations.
#[derive(Clone, Debug, Default)]
pub struct StakingMaterializer;
impl crate::Materializer for crate::StakingMaterializer {
fn materializer_name(&self) -> &'static str {
return "kb_materializer_staking";
}
fn materializer_version(&self) -> &'static str {
return env!("CARGO_PKG_VERSION");
}
fn accepts_event(&self, event: &crate::DecodedProtocolEvent) -> bool {
return family_is_accepted(event.event_family)
&& projection(event.surface_code.0.as_str(), event.event_name.0.as_str()).is_some();
}
fn materialize_event(
&self,
event: &crate::DecodedProtocolEvent,
) -> kb_core::Result<std::vec::Vec<crate::MaterializedEvent>> {
if !crate::Materializer::accepts_event(self, event) {
return std::result::Result::Ok(std::vec::Vec::new());
}
return std::result::Result::Ok(std::vec::Vec::new());
}
}
impl crate::EventMaterializer for crate::StakingMaterializer {
fn identity(&self) -> crate::MaterializerIdentity {
return crate::MaterializerIdentity {
name: "solana_native_staking".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
};
}
fn accepted_families(&self) -> &'static [crate::EventFamily] {
return ACCEPTED_FAMILIES;
}
fn accepts_observation(&self, observation: &crate::DecodedObservation) -> bool {
return family_is_accepted(observation.event.event_family)
&& projection(
observation.event.surface_code.0.as_str(),
observation.event.event_name.0.as_str(),
)
.is_some();
}
fn transaction_policy(
&self,
_family: crate::EventFamily,
) -> crate::MaterializationTransactionPolicy {
return crate::MaterializationTransactionPolicy::SuccessfulCommittedOnly;
}
fn materialize(
&self,
observation: &crate::DecodedObservation,
) -> crate::MaterializerExecutionResult {
let projection = match projection(
observation.event.surface_code.0.as_str(),
observation.event.event_name.0.as_str(),
) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
tracing::debug!(
target: crate::MATERIALIZER_STAKING_TRACING_TARGET,
action = "materialize_staking",
surface_code = %observation.event.surface_code.0.as_str(),
entry_code = %observation.event.event_name.0.as_str(),
accepted = false,
"ignore observation outside supported native staking projections"
);
return crate::MaterializerExecutionResult::ignored();
},
};
if !family_is_accepted(observation.event.event_family) {
return crate::MaterializerExecutionResult::ignored();
}
if observation.transaction_failed || !observation.observation_committed {
tracing::debug!(
target: crate::MATERIALIZER_STAKING_TRACING_TARGET,
action = "materialize_staking",
surface_code = %observation.event.surface_code.0.as_str(),
entry_code = %observation.event.event_name.0.as_str(),
committed = observation.observation_committed,
transaction_failed = observation.transaction_failed,
"refuse uncommitted native staking observation"
);
return crate::MaterializerExecutionResult::refused(
"failed_transaction_staking_refused",
"failed or uncommitted native Stake/Vote observations cannot create staking outputs",
);
}
let accounts = observation
.payload_json
.get("accounts")
.cloned()
.unwrap_or(serde_json::Value::Null);
let parameters = observation
.payload_json
.get("parameters")
.cloned()
.unwrap_or(serde_json::Value::Null);
let operation = observation.event.event_name.0.clone();
let output = crate::MaterializedOutput {
output_key: projection.output_key(operation.as_str()),
family: projection.family(),
payload_json: serde_json::json!({
"projectionVersion": 1,
"projectionSemantics": "committed_instruction_staking_event",
"domain": projection.domain(),
"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(),
"transactionSucceeded": true,
"accounts": accounts,
"parameters": parameters,
"projection": projection_details(
projection,
&accounts,
&parameters,
observation.event.event_name.0.as_str(),
),
}),
};
tracing::debug!(
target: crate::MATERIALIZER_STAKING_TRACING_TARGET,
action = "materialize_staking",
surface_code = %observation.event.surface_code.0.as_str(),
entry_code = %observation.event.event_name.0.as_str(),
projection_domain = projection.domain(),
output_count = 1_usize,
"materialize committed native staking observation"
);
return crate::MaterializerExecutionResult {
status: crate::MaterializerOutcomeStatus::Inserted,
outputs: std::vec![output],
diagnostics: std::vec::Vec::new(),
};
}
}
fn family_is_accepted(family: crate::EventFamily) -> bool {
return ACCEPTED_FAMILIES.iter().any(|accepted| return *accepted == family);
}
fn projection(surface_code: &str, entry_code: &str) -> std::option::Option<ProjectionKind> {
if surface_code == STAKE_SURFACE {
return stake_projection(entry_code);
}
if surface_code == VOTE_SURFACE {
return vote_projection(entry_code);
}
return std::option::Option::None;
}
fn stake_projection(entry_code: &str) -> std::option::Option<ProjectionKind> {
if contains(STAKE_ACCOUNT_ENTRIES, entry_code) {
return std::option::Option::Some(ProjectionKind::StakeAccount);
}
if contains(STAKE_VALUE_ENTRIES, entry_code) {
return std::option::Option::Some(ProjectionKind::StakeValue);
}
if contains(STAKE_AUTHORITY_ENTRIES, entry_code) {
return std::option::Option::Some(ProjectionKind::StakeAuthority);
}
if contains(STAKE_LOCKUP_ENTRIES, entry_code) {
return std::option::Option::Some(ProjectionKind::StakeLockup);
}
return std::option::Option::None;
}
fn vote_projection(entry_code: &str) -> std::option::Option<ProjectionKind> {
if contains(VOTE_ACCOUNT_ENTRIES, entry_code) {
return std::option::Option::Some(ProjectionKind::VoteAccount);
}
if contains(VOTE_AUTHORITY_ENTRIES, entry_code) {
return std::option::Option::Some(ProjectionKind::VoteAuthority);
}
if contains(VOTE_ADMIN_ENTRIES, entry_code) {
return std::option::Option::Some(ProjectionKind::VoteAdmin);
}
if contains(VOTE_STATE_ENTRIES, entry_code) {
return std::option::Option::Some(ProjectionKind::VoteState);
}
if contains(VOTE_VALUE_ENTRIES, entry_code) {
return std::option::Option::Some(ProjectionKind::VoteValue);
}
if contains(VOTE_REWARD_ENTRIES, entry_code) {
return std::option::Option::Some(ProjectionKind::VoteReward);
}
return std::option::Option::None;
}
fn projection_details(
projection: ProjectionKind,
accounts: &serde_json::Value,
parameters: &serde_json::Value,
operation: &str,
) -> serde_json::Value {
return match projection {
ProjectionKind::StakeAccount => stake_account_details(accounts, parameters, operation),
ProjectionKind::StakeValue => stake_value_details(accounts, parameters, operation),
ProjectionKind::StakeAuthority => stake_authority_details(accounts, parameters, operation),
ProjectionKind::StakeLockup => stake_lockup_details(accounts, parameters, operation),
ProjectionKind::VoteAccount => vote_account_details(accounts, parameters, operation),
ProjectionKind::VoteAuthority => vote_authority_details(accounts, parameters, operation),
ProjectionKind::VoteAdmin => vote_admin_details(accounts, parameters, operation),
ProjectionKind::VoteState => vote_state_details(accounts, parameters, operation),
ProjectionKind::VoteValue => vote_value_details(accounts, parameters, operation),
ProjectionKind::VoteReward => vote_reward_details(accounts, parameters, operation),
};
}
fn stake_account_details(
accounts: &serde_json::Value,
parameters: &serde_json::Value,
operation: &str,
) -> serde_json::Value {
return serde_json::json!({
"stakeAccount": account_key_for_role(accounts, "stake_account"),
"voteAccount": account_key_for_role(accounts, "vote_account"),
"authorized": parameters.get("authorized").cloned().unwrap_or(serde_json::Value::Null),
"lockup": parameters.get("lockup").cloned().unwrap_or(serde_json::Value::Null),
"activationStateComputed": false,
"transactionFinalAccountStateCaptured": false,
"stateTransition": match operation {
"initialize" | "initialize_checked" => "stake_account_initialized",
"delegate_stake" => "stake_delegation_requested",
"deactivate" => "stake_deactivation_requested",
"deactivate_delinquent" => "stake_deactivation_requested_for_delinquent_vote_account",
_ => "stake_account_state_transition_requested",
},
});
}
fn stake_value_details(
accounts: &serde_json::Value,
parameters: &serde_json::Value,
operation: &str,
) -> serde_json::Value {
return serde_json::json!({
"stakeAccount": account_key_for_role(accounts, "stake_account"),
"splitAccount": account_key_for_role(accounts, "split_account"),
"destinationAccount": first_account_key_for_roles(accounts, &["destination", "recipient_account", "destination_stake_account"]),
"sourceStakeAccount": account_key_for_role(accounts, "source_stake_account"),
"lamports": parameters.get("lamports").cloned().unwrap_or(serde_json::Value::Null),
"lamportBalanceChangesDuplicated": false,
"activationStateComputed": false,
"transactionFinalAccountStateCaptured": false,
"stateTransition": match operation {
"split" => "stake_split_requested",
"withdraw" => "stake_lamports_withdrawn_or_account_closed_conditionally",
"merge" => "stake_accounts_merged",
"move_stake" => "active_stake_moved",
"move_lamports" => "inactive_lamports_moved",
_ => "stake_value_transition_requested",
},
});
}
fn stake_authority_details(
accounts: &serde_json::Value,
parameters: &serde_json::Value,
operation: &str,
) -> serde_json::Value {
return serde_json::json!({
"stakeAccount": account_key_for_role(accounts, "stake_account"),
"currentAuthority": first_account_key_for_roles(accounts, &["stake_or_withdraw_authority", "current_authority", "stake_authority", "withdraw_authority", "authority_base"]),
"newAuthority": first_parameter_or_account(
parameters,
&["newAuthority", "newAuthorizedPubkey"],
accounts,
"new_authority",
),
"authorizationType": parameters.get("authorizationType").cloned().unwrap_or(serde_json::Value::Null),
"authoritySeed": parameters.get("authoritySeed").cloned().unwrap_or(serde_json::Value::Null),
"authorityOwner": parameters.get("authorityOwner").cloned().unwrap_or(serde_json::Value::Null),
"stateTransition": match operation {
"authorize_with_seed" | "authorize_checked_with_seed" => "stake_authority_changed_with_seed",
"authorize_checked" => "stake_authority_changed_with_new_authority_signature",
_ => "stake_authority_changed",
},
"transactionFinalAccountStateCaptured": false,
});
}
fn stake_lockup_details(
accounts: &serde_json::Value,
parameters: &serde_json::Value,
operation: &str,
) -> serde_json::Value {
return serde_json::json!({
"stakeAccount": account_key_for_role(accounts, "stake_account"),
"custodian": first_account_key_for_roles(accounts, &["lockup_custodian", "lockup_or_withdraw_authority", "new_lockup_authority"]),
"lockup": parameters.clone(),
"checkedCustodianFromAccount": operation == "set_lockup_checked",
"stateTransition": "stake_lockup_updated",
"transactionFinalAccountStateCaptured": false,
});
}
fn vote_account_details(
accounts: &serde_json::Value,
parameters: &serde_json::Value,
operation: &str,
) -> serde_json::Value {
return serde_json::json!({
"voteAccount": account_key_for_role(accounts, "vote_account"),
"validatorIdentity": account_key_for_role(accounts, "validator_identity"),
"authorizedVoter": parameters.get("authorizedVoter").cloned().unwrap_or(serde_json::Value::Null),
"authorizedWithdrawer": parameters.get("authorizedWithdrawer").cloned().unwrap_or(serde_json::Value::Null),
"commissionPercent": parameters.get("commissionPercent").cloned().unwrap_or(serde_json::Value::Null),
"inflationRewardsCommissionBasisPoints": parameters.get("inflationRewardsCommissionBasisPoints").cloned().unwrap_or(serde_json::Value::Null),
"blockRevenueCommissionBasisPoints": parameters.get("blockRevenueCommissionBasisPoints").cloned().unwrap_or(serde_json::Value::Null),
"inflationRewardsCollector": account_key_for_role(accounts, "inflation_rewards_collector"),
"blockRevenueCollector": account_key_for_role(accounts, "block_revenue_collector"),
"stateTransition": if operation == "initialize_account_v2" {
"vote_account_initialized_v2"
} else {
"vote_account_initialized"
},
"transactionFinalAccountStateCaptured": false,
});
}
fn vote_authority_details(
accounts: &serde_json::Value,
parameters: &serde_json::Value,
operation: &str,
) -> serde_json::Value {
return serde_json::json!({
"voteAccount": account_key_for_role(accounts, "vote_account"),
"currentAuthority": first_account_key_for_roles(accounts, &["current_authority", "current_authority_base"]),
"newAuthority": first_parameter_or_account(
parameters,
&["newAuthority"],
accounts,
"new_authority",
),
"authorizationType": parameters.get("authorizationType").cloned().unwrap_or(serde_json::Value::Null),
"currentAuthorityDerivedKeyOwner": parameters.get("currentAuthorityDerivedKeyOwner").cloned().unwrap_or(serde_json::Value::Null),
"currentAuthorityDerivedKeySeed": parameters.get("currentAuthorityDerivedKeySeed").cloned().unwrap_or(serde_json::Value::Null),
"newAuthorityFromAccount": parameters.get("newAuthorityFromAccount").cloned().unwrap_or(serde_json::Value::Bool(false)),
"stateTransition": match operation {
"authorize_with_seed" | "authorize_checked_with_seed" => "vote_authority_changed_with_seed",
"authorize_checked" => "vote_authority_changed_with_new_authority_signature",
_ => "vote_authority_changed",
},
"transactionFinalAccountStateCaptured": false,
});
}
fn vote_admin_details(
accounts: &serde_json::Value,
parameters: &serde_json::Value,
operation: &str,
) -> serde_json::Value {
return serde_json::json!({
"voteAccount": account_key_for_role(accounts, "vote_account"),
"withdrawAuthority": account_key_for_role(accounts, "withdraw_authority"),
"newValidatorIdentity": account_key_for_role(accounts, "new_validator_identity"),
"newCommissionCollector": account_key_for_role(accounts, "new_commission_collector"),
"commissionPercent": parameters.get("commissionPercent").cloned().unwrap_or(serde_json::Value::Null),
"commissionBasisPoints": parameters.get("commissionBasisPoints").cloned().unwrap_or(serde_json::Value::Null),
"commissionKind": parameters.get("commissionKind").cloned().unwrap_or(serde_json::Value::Null),
"stateTransition": match operation {
"update_validator_identity" => "vote_validator_identity_updated",
"update_commission" | "update_commission_bps" => "vote_commission_updated",
"update_commission_collector" => "vote_commission_collector_updated",
_ => "vote_admin_updated",
},
"transactionFinalAccountStateCaptured": false,
});
}
fn vote_state_details(
accounts: &serde_json::Value,
parameters: &serde_json::Value,
operation: &str,
) -> serde_json::Value {
return serde_json::json!({
"voteAccount": account_key_for_role(accounts, "vote_account"),
"voteAuthority": account_key_for_role(accounts, "vote_authority"),
"lastVotedSlot": parameters.get("lastVotedSlot").cloned().unwrap_or(serde_json::Value::Null),
"slots": parameters.get("slots").cloned().unwrap_or(serde_json::Value::Null),
"root": parameters.get("root").cloned().unwrap_or(serde_json::Value::Null),
"hash": first_parameter_value(parameters, &["hash", "bankHash"]),
"timestamp": parameters.get("timestamp").cloned().unwrap_or(serde_json::Value::Null),
"switchProofHash": parameters.get("switchProofHash").cloned().unwrap_or(serde_json::Value::Null),
"stateTransition": if operation.contains("switch") {
"vote_state_updated_with_switch_proof"
} else if operation.starts_with("tower_sync") {
"vote_tower_synchronized"
} else {
"vote_state_updated"
},
"voteCreditsComputed": false,
"transactionFinalAccountStateCaptured": false,
});
}
fn vote_value_details(
accounts: &serde_json::Value,
parameters: &serde_json::Value,
_operation: &str,
) -> serde_json::Value {
return serde_json::json!({
"voteAccount": account_key_for_role(accounts, "vote_account"),
"recipientAccount": account_key_for_role(accounts, "recipient_account"),
"withdrawAuthority": account_key_for_role(accounts, "withdraw_authority"),
"lamports": parameters.get("lamports").cloned().unwrap_or(serde_json::Value::Null),
"lamportBalanceChangesDuplicated": false,
"stateTransition": "vote_lamports_withdrawn_or_account_closed_conditionally",
"transactionFinalAccountStateCaptured": false,
});
}
fn vote_reward_details(
accounts: &serde_json::Value,
parameters: &serde_json::Value,
_operation: &str,
) -> serde_json::Value {
return serde_json::json!({
"voteAccount": account_key_for_role(accounts, "vote_account"),
"depositSource": account_key_for_role(accounts, "deposit_source"),
"depositLamports": parameters.get("depositLamports").cloned().unwrap_or(serde_json::Value::Null),
"lamportBalanceChangesDuplicated": false,
"stateTransition": "delegator_rewards_deposited_into_vote_account",
"transactionFinalAccountStateCaptured": false,
});
}
fn first_parameter_or_account(
parameters: &serde_json::Value,
keys: &[&str],
accounts: &serde_json::Value,
fallback_role: &str,
) -> serde_json::Value {
let value = first_parameter_value(parameters, keys);
if !value.is_null() {
return value;
}
return account_key_for_role(accounts, fallback_role);
}
fn first_parameter_value(parameters: &serde_json::Value, keys: &[&str]) -> serde_json::Value {
for key in keys {
if let std::option::Option::Some(value) = parameters.get(*key) {
return value.clone();
}
}
return serde_json::Value::Null;
}
fn first_account_key_for_roles(accounts: &serde_json::Value, roles: &[&str]) -> serde_json::Value {
for role in roles {
let value = account_key_for_role(accounts, role);
if !value.is_null() {
return value;
}
}
return serde_json::Value::Null;
}
fn account_key_for_role(accounts: &serde_json::Value, role: &str) -> serde_json::Value {
let account_key = accounts.as_array().and_then(|values| {
return values.iter().find_map(|value| {
if value.get("role").and_then(serde_json::Value::as_str)
!= std::option::Option::Some(role)
{
return std::option::Option::None;
}
return value
.get("accountKey")
.and_then(serde_json::Value::as_str)
.map(|account_key| return account_key.to_string());
});
});
return match account_key {
std::option::Option::Some(value) => serde_json::Value::String(value),
std::option::Option::None => serde_json::Value::Null,
};
}
fn contains(entries: &[&str], entry_code: &str) -> bool {
return entries.iter().any(|entry| return *entry == entry_code);
}
#[cfg(test)]
mod tests {
fn observation(
surface_code: &str,
entry_code: &str,
family: crate::EventFamily,
transaction_failed: bool,
accounts: serde_json::Value,
parameters: serde_json::Value,
) -> crate::DecodedObservation {
return crate::DecodedObservation {
event_key: format!("{entry_code}:0"),
event: crate::DecodedProtocolEvent {
signature: crate::Signature("signature".to_string()),
slot: crate::Slot(42),
instruction_path: crate::InstructionPath("0".to_string()),
program_id: crate::ProgramId("program-id".to_string()),
protocol_code: crate::ProtocolCode("solana_native".to_string()),
surface_code: crate::SurfaceCode(surface_code.to_string()),
event_code: crate::EventCode(format!("{surface_code}.{entry_code}")),
event_name: crate::EventName(entry_code.to_string()),
event_family: family,
source_kind: crate::EventSourceKind::Instruction,
confidence: crate::DecoderConfidence::ManualExact,
},
payload_json: serde_json::json!({"accounts": accounts, "parameters": parameters}),
transaction_failed,
transaction_error: if transaction_failed {
std::option::Option::Some(serde_json::json!({"InstructionError": [0, "Custom"]}))
} else {
std::option::Option::None
},
observation_committed: !transaction_failed,
proof: crate::DecoderProof {
kind: crate::DecoderProofKind::Manual,
confidence: crate::DecoderConfidence::ManualExact,
evidence: std::vec!["fixture".to_string()],
},
};
}
fn account(role: &str, account_key: &str) -> serde_json::Value {
return serde_json::json!({"role": role, "accountKey": account_key});
}
#[test]
fn stake_initialize_creates_instructional_staking_projection() {
let materializer = crate::StakingMaterializer;
let observation = observation(
super::STAKE_SURFACE,
"initialize",
crate::EventFamily::Lifecycle,
false,
serde_json::json!([account("stake_account", "stake111")]),
serde_json::json!({"authorized": {"staker": "staker111"}}),
);
let result = crate::EventMaterializer::materialize(&materializer, &observation);
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Inserted);
assert_eq!(result.outputs[0].family, crate::MaterializedEventFamily::Staking);
assert_eq!(result.outputs[0].output_key, "stake_account:initialize:0");
assert_eq!(result.outputs[0].payload_json["projection"]["stakeAccount"], "stake111");
assert_eq!(
result.outputs[0].payload_json["projection"]["transactionFinalAccountStateCaptured"],
false
);
}
#[test]
fn stake_authority_and_lockup_events_are_projected() {
let materializer = crate::StakingMaterializer;
let authority = observation(
super::STAKE_SURFACE,
"authorize_with_seed",
crate::EventFamily::Admin,
false,
serde_json::json!([account("stake_account", "stake111")]),
serde_json::json!({
"newAuthorizedPubkey": "new111",
"authorizationType": "staker",
"authoritySeed": "seed",
"authorityOwner": "owner111",
}),
);
let lockup = observation(
super::STAKE_SURFACE,
"set_lockup_checked",
crate::EventFamily::Admin,
false,
serde_json::json!([
account("stake_account", "stake111"),
account("lockup_custodian", "custodian111"),
]),
serde_json::json!({"epoch": 42}),
);
let authority_result = crate::EventMaterializer::materialize(&materializer, &authority);
let lockup_result = crate::EventMaterializer::materialize(&materializer, &lockup);
assert_eq!(authority_result.outputs[0].output_key, "stake_authority:authorize_with_seed:0");
assert_eq!(
authority_result.outputs[0].payload_json["projection"]["newAuthority"],
"new111"
);
assert_eq!(lockup_result.outputs[0].output_key, "stake_lockup:set_lockup_checked:0");
assert_eq!(
lockup_result.outputs[0].payload_json["projection"]["checkedCustodianFromAccount"],
true
);
}
#[test]
fn stake_value_projection_does_not_duplicate_lamport_changes() {
let materializer = crate::StakingMaterializer;
let observation = observation(
super::STAKE_SURFACE,
"withdraw",
crate::EventFamily::Staking,
false,
serde_json::json!([
account("stake_account", "stake111"),
account("destination", "dest111"),
]),
serde_json::json!({"lamports": 99}),
);
let result = crate::EventMaterializer::materialize(&materializer, &observation);
assert_eq!(result.outputs[0].output_key, "stake_value:withdraw:0");
assert_eq!(result.outputs[0].payload_json["projection"]["lamports"], 99);
assert_eq!(
result.outputs[0].payload_json["projection"]["lamportBalanceChangesDuplicated"],
false
);
}
#[test]
fn vote_account_and_authority_events_are_projected() {
let materializer = crate::StakingMaterializer;
let initialize = observation(
super::VOTE_SURFACE,
"initialize_account_v2",
crate::EventFamily::Lifecycle,
false,
serde_json::json!([
account("vote_account", "vote111"),
account("validator_identity", "validator111"),
]),
serde_json::json!({
"authorizedVoter": "voter111",
"authorizedWithdrawer": "withdrawer111",
"inflationRewardsCommissionBasisPoints": 500,
}),
);
let authorize = observation(
super::VOTE_SURFACE,
"authorize_checked",
crate::EventFamily::Admin,
false,
serde_json::json!([
account("vote_account", "vote111"),
account("new_authority", "new111"),
]),
serde_json::json!({"authorizationType": {"kind": "voter"}, "newAuthorityFromAccount": true}),
);
let initialize_result = crate::EventMaterializer::materialize(&materializer, &initialize);
let authorize_result = crate::EventMaterializer::materialize(&materializer, &authorize);
assert_eq!(initialize_result.outputs[0].output_key, "vote_account:initialize_account_v2:0");
assert_eq!(
initialize_result.outputs[0].payload_json["projection"]["validatorIdentity"],
"validator111"
);
assert_eq!(authorize_result.outputs[0].output_key, "vote_authority:authorize_checked:0");
assert_eq!(
authorize_result.outputs[0].payload_json["projection"]["newAuthority"],
"new111"
);
}
#[test]
fn vote_state_and_reward_outputs_are_distinguished() {
let materializer = crate::StakingMaterializer;
let vote_state = observation(
super::VOTE_SURFACE,
"tower_sync_switch",
crate::EventFamily::Staking,
false,
serde_json::json!([
account("vote_account", "vote111"),
account("vote_authority", "authority111"),
]),
serde_json::json!({"root": 10, "hash": "hash111", "switchProofHash": "switch111"}),
);
let reward = observation(
super::VOTE_SURFACE,
"deposit_delegator_rewards",
crate::EventFamily::Reward,
false,
serde_json::json!([
account("vote_account", "vote111"),
account("deposit_source", "source111"),
]),
serde_json::json!({"depositLamports": 123}),
);
let vote_result = crate::EventMaterializer::materialize(&materializer, &vote_state);
let reward_result = crate::EventMaterializer::materialize(&materializer, &reward);
assert_eq!(vote_result.outputs[0].output_key, "vote_state:tower_sync_switch:0");
assert_eq!(
vote_result.outputs[0].payload_json["projection"]["switchProofHash"],
"switch111"
);
assert_eq!(reward_result.outputs[0].family, crate::MaterializedEventFamily::Reward);
assert_eq!(reward_result.outputs[0].output_key, "vote_reward:deposit_delegator_rewards:0");
}
#[test]
fn failed_staking_mutation_is_refused() {
let materializer = crate::StakingMaterializer;
let observation = observation(
super::STAKE_SURFACE,
"delegate_stake",
crate::EventFamily::Staking,
true,
serde_json::json!([account("stake_account", "stake111")]),
serde_json::json!({}),
);
let result = crate::EventMaterializer::materialize(&materializer, &observation);
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Refused);
assert!(result.outputs.is_empty());
}
#[test]
fn unrelated_or_disabled_observations_are_ignored() {
let materializer = crate::StakingMaterializer;
let unrelated = observation(
"solana_native_system",
"create_account",
crate::EventFamily::Lifecycle,
false,
serde_json::json!([]),
serde_json::json!({}),
);
let redelegate = observation(
super::STAKE_SURFACE,
"redelegate",
crate::EventFamily::Staking,
false,
serde_json::json!([]),
serde_json::json!({"officiallyDisabled": true}),
);
let unrelated_result = crate::EventMaterializer::materialize(&materializer, &unrelated);
let redelegate_result = crate::EventMaterializer::materialize(&materializer, &redelegate);
assert_eq!(unrelated_result.status, crate::MaterializerOutcomeStatus::Ignored);
assert_eq!(redelegate_result.status, crate::MaterializerOutcomeStatus::Ignored);
}
#[test]
fn output_serialization_is_deterministic() {
let materializer = crate::StakingMaterializer;
let observation = observation(
super::VOTE_SURFACE,
"update_commission_bps",
crate::EventFamily::Admin,
false,
serde_json::json!([account("vote_account", "vote111")]),
serde_json::json!({"commissionBasisPoints": 250, "commissionKind": "inflation_rewards"}),
);
let first = crate::EventMaterializer::materialize(&materializer, &observation);
let second = crate::EventMaterializer::materialize(&materializer, &observation);
let first_json_result = serde_json::to_string(&first.outputs[0].payload_json);
let first_json = match first_json_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("unexpected serialization error: {error}"),
};
let second_json_result = serde_json::to_string(&second.outputs[0].payload_json);
let second_json = match second_json_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("unexpected serialization error: {error}"),
};
assert_eq!(first_json, second_json);
}
}

View File

@@ -0,0 +1,42 @@
// file: kb-lib/tests/external_materializer_api.rs
// version: 1
//! Downstream-style compilation contract for externally implemented materializers.
const ACCEPTED_FAMILIES: &[kb_lib::EventFamily] = &[kb_lib::EventFamily::Audit];
struct ExternalAuditMaterializer;
impl kb_lib::EventMaterializer for ExternalAuditMaterializer {
fn identity(&self) -> kb_lib::MaterializerIdentity {
return kb_lib::MaterializerIdentity {
name: "external_audit_materializer".to_string(),
version: "1".to_string(),
};
}
fn accepted_families(&self) -> &'static [kb_lib::EventFamily] {
return ACCEPTED_FAMILIES;
}
fn transaction_policy(
&self,
_family: kb_lib::EventFamily,
) -> kb_lib::MaterializationTransactionPolicy {
return kb_lib::MaterializationTransactionPolicy::SuccessfulOrFailedAudit;
}
fn materialize(
&self,
_observation: &kb_lib::DecodedObservation,
) -> kb_lib::MaterializerExecutionResult {
return kb_lib::MaterializerExecutionResult::ignored();
}
}
#[test]
fn external_materializer_uses_only_the_public_kb_lib_contract() {
let materializer: &dyn kb_lib::EventMaterializer = &ExternalAuditMaterializer;
assert_eq!(materializer.identity().name, "external_audit_materializer");
assert_eq!(materializer.accepted_families(), ACCEPTED_FAMILIES);
}