From 44088c71c9e640f99936edbcf1cf92de74f7c54e Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Thu, 6 Aug 2026 00:52:08 +0200 Subject: [PATCH] v0.4.8-pre.007 --- Cargo.toml | 4 +- ROADMAP.md | 5 +- config/README.md | 7 +- config/example.config.json | 162 +-- docs/OPERATION_NAMING_CONVENTION.md | 12 +- docs/README.md | 8 +- .../V0_4_8_PRE_002_METADATA_CONTRACT_AUDIT.md | 62 +- ...8_PRE_007_MATERIALIZER_CONVENTION_AUDIT.md | 189 +++ ...7_METAPLEX_MATERIALIZER_STRUCTURE_AUDIT.md | 109 ++ docs/guides/LOGGING.md | 7 +- ...TADATA_AND_TOKEN_2022_COMPLETENESS_PLAN.md | 43 +- docs/rules/RULES_SPECIFIC_KHADHROONY.md | 13 +- kb-lib/CHANGELOG.md | 28 +- kb-lib/README.md | 28 +- kb-lib/TODO.md | 16 +- kb-lib/USAGE.md | 41 +- .../metaplex_token_metadata/decoder.rs | 17 +- .../solana_program_metadata/account.rs | 79 +- .../solana_program_metadata/instruction.rs | 10 +- .../metadata/solana_program_metadata/model.rs | 204 ++- kb-lib/src/executor.rs | 61 +- kb-lib/src/executor/metadata.rs | 56 +- .../metadata/solana_program_metadata.rs | 104 +- .../solana_program_metadata/builder.rs | 1270 +++++++++++++++++ .../solana_program_metadata/constants.rs | 55 + .../solana_program_metadata/executor.rs | 224 +++ .../solana_program_metadata/intent.rs | 471 ++++++ kb-lib/src/lib.rs | 378 ++++- kb-lib/src/materializer.rs | 571 +++++++- kb-lib/src/materializer/admin.rs | 50 +- kb-lib/src/materializer/admin/constants.rs | 66 +- .../admin/{core.rs => materializer.rs} | 140 +- kb-lib/src/materializer/api/contracts.rs | 6 +- kb-lib/src/materializer/api/materializer.rs | 8 +- kb-lib/src/materializer/bridge.rs | 14 +- kb-lib/src/materializer/bridge/constants.rs | 11 + .../bridge/{core.rs => materializer.rs} | 19 +- kb-lib/src/materializer/compliance.rs | 29 +- kb-lib/src/materializer/compliance/audit.rs | 604 +------- .../compliance/audit/constants.rs | 42 + .../compliance/audit/materializer.rs | 581 ++++++++ .../src/materializer/compliance/constants.rs | 8 - kb-lib/src/materializer/fees.rs | 24 +- kb-lib/src/materializer/fees/constants.rs | 34 + .../fees/{core.rs => materializer.rs} | 105 +- kb-lib/src/materializer/governance.rs | 14 +- .../src/materializer/governance/constants.rs | 11 + .../governance/{core.rs => materializer.rs} | 19 +- kb-lib/src/materializer/lending.rs | 14 +- kb-lib/src/materializer/lending/constants.rs | 11 + .../lending/{core.rs => materializer.rs} | 19 +- kb-lib/src/materializer/lifecycle.rs | 54 +- .../src/materializer/lifecycle/constants.rs | 99 +- .../lifecycle/{core.rs => materializer.rs} | 174 +-- kb-lib/src/materializer/liquidity.rs | 14 +- .../src/materializer/liquidity/constants.rs | 11 + .../liquidity/{core.rs => materializer.rs} | 19 +- kb-lib/src/materializer/metadata.rs | 43 +- kb-lib/src/materializer/metadata/core.rs | 608 -------- .../metadata/metaplex_token_metadata.rs | 22 + .../metaplex_token_metadata/account.rs | 283 ++++ .../metaplex_token_metadata/constants.rs | 16 + .../metadata/metaplex_token_metadata/state.rs | 229 +++ .../metadata/solana_program_metadata.rs | 16 +- .../solana_program_metadata/constants.rs | 18 +- .../{instruction.rs => materializer.rs} | 166 ++- .../metadata/solana_program_metadata/state.rs | 42 +- .../src/materializer/metadata/token_2022.rs | 19 + .../metadata/token_2022/constants.rs | 15 + .../materializer/metadata/token_2022/state.rs | 210 +++ kb-lib/src/materializer/nft.rs | 14 +- kb-lib/src/materializer/nft/constants.rs | 11 + .../nft/{core.rs => materializer.rs} | 19 +- kb-lib/src/materializer/oracle.rs | 14 +- kb-lib/src/materializer/oracle/constants.rs | 11 + .../oracle/{core.rs => materializer.rs} | 19 +- kb-lib/src/materializer/orderbook.rs | 14 +- .../src/materializer/orderbook/constants.rs | 11 + .../orderbook/{core.rs => materializer.rs} | 19 +- kb-lib/src/materializer/perpetuals.rs | 14 +- .../src/materializer/perpetuals/constants.rs | 11 + .../perpetuals/{core.rs => materializer.rs} | 19 +- kb-lib/src/materializer/pool.rs | 9 +- kb-lib/src/materializer/pool/state.rs | 68 +- .../src/materializer/pool/state/constants.rs | 11 + .../materializer/pool/state/materializer.rs | 56 + kb-lib/src/materializer/rewards.rs | 14 +- kb-lib/src/materializer/rewards/constants.rs | 11 + .../rewards/{core.rs => materializer.rs} | 21 +- kb-lib/src/materializer/risk.rs | 18 +- kb-lib/src/materializer/risk/constants.rs | 45 +- .../risk/{core.rs => materializer.rs} | 46 +- kb-lib/src/materializer/routing.rs | 14 +- kb-lib/src/materializer/routing/constants.rs | 11 + .../routing/{core.rs => materializer.rs} | 19 +- kb-lib/src/materializer/staking.rs | 40 +- kb-lib/src/materializer/staking/constants.rs | 76 +- .../staking/{core.rs => materializer.rs} | 115 +- kb-lib/src/materializer/token.rs | 23 +- kb-lib/src/materializer/token/accounts.rs | 859 +---------- .../materializer/token/accounts/constants.rs | 54 + .../token/accounts/materializer.rs | 809 +++++++++++ kb-lib/src/materializer/token/constants.rs | 11 - .../src/materializer/token/metadata_risk.rs | 68 +- .../token/metadata_risk/constants.rs | 12 + .../token/metadata_risk/materializer.rs | 56 + kb-lib/src/materializer/trades.rs | 14 +- kb-lib/src/materializer/trades/constants.rs | 11 + .../trades/{core.rs => materializer.rs} | 21 +- kb-lib/src/materializer/transaction.rs | 17 +- .../materializer/transaction/annotations.rs | 626 +------- .../transaction/annotations/constants.rs | 25 + .../transaction/annotations/materializer.rs | 609 ++++++++ .../src/materializer/transaction/constants.rs | 15 - kb-lib/src/materializer/vault.rs | 14 +- kb-lib/src/materializer/vault/constants.rs | 11 + .../vault/{core.rs => materializer.rs} | 19 +- kb-lib/tests/external_materializer_api.rs | 8 +- ...al_solana_program_metadata_executor_api.rs | 63 + scripts/audit_khadhroony_workspace_rules.py | 383 ++++- .../OPERATION_NAMING_MATRIX.json | 99 ++ ...LANA_PROGRAM_METADATA_EXECUTOR_MATRIX.json | 119 ++ 122 files changed, 8903 insertions(+), 3914 deletions(-) create mode 100644 docs/audits/V0_4_8_PRE_007_MATERIALIZER_CONVENTION_AUDIT.md create mode 100644 docs/audits/V0_4_8_PRE_007_METAPLEX_MATERIALIZER_STRUCTURE_AUDIT.md create mode 100644 kb-lib/src/executor/metadata/solana_program_metadata/builder.rs create mode 100644 kb-lib/src/executor/metadata/solana_program_metadata/constants.rs create mode 100644 kb-lib/src/executor/metadata/solana_program_metadata/executor.rs create mode 100644 kb-lib/src/executor/metadata/solana_program_metadata/intent.rs rename kb-lib/src/materializer/admin/{core.rs => materializer.rs} (92%) create mode 100644 kb-lib/src/materializer/bridge/constants.rs rename kb-lib/src/materializer/bridge/{core.rs => materializer.rs} (72%) create mode 100644 kb-lib/src/materializer/compliance/audit/constants.rs create mode 100644 kb-lib/src/materializer/compliance/audit/materializer.rs delete mode 100644 kb-lib/src/materializer/compliance/constants.rs create mode 100644 kb-lib/src/materializer/fees/constants.rs rename kb-lib/src/materializer/fees/{core.rs => materializer.rs} (81%) create mode 100644 kb-lib/src/materializer/governance/constants.rs rename kb-lib/src/materializer/governance/{core.rs => materializer.rs} (72%) create mode 100644 kb-lib/src/materializer/lending/constants.rs rename kb-lib/src/materializer/lending/{core.rs => materializer.rs} (72%) rename kb-lib/src/materializer/lifecycle/{core.rs => materializer.rs} (88%) create mode 100644 kb-lib/src/materializer/liquidity/constants.rs rename kb-lib/src/materializer/liquidity/{core.rs => materializer.rs} (72%) delete mode 100644 kb-lib/src/materializer/metadata/core.rs create mode 100644 kb-lib/src/materializer/metadata/metaplex_token_metadata.rs create mode 100644 kb-lib/src/materializer/metadata/metaplex_token_metadata/account.rs create mode 100644 kb-lib/src/materializer/metadata/metaplex_token_metadata/constants.rs create mode 100644 kb-lib/src/materializer/metadata/metaplex_token_metadata/state.rs rename kb-lib/src/materializer/metadata/solana_program_metadata/{instruction.rs => materializer.rs} (71%) create mode 100644 kb-lib/src/materializer/metadata/token_2022.rs create mode 100644 kb-lib/src/materializer/metadata/token_2022/constants.rs create mode 100644 kb-lib/src/materializer/metadata/token_2022/state.rs create mode 100644 kb-lib/src/materializer/nft/constants.rs rename kb-lib/src/materializer/nft/{core.rs => materializer.rs} (72%) create mode 100644 kb-lib/src/materializer/oracle/constants.rs rename kb-lib/src/materializer/oracle/{core.rs => materializer.rs} (72%) create mode 100644 kb-lib/src/materializer/orderbook/constants.rs rename kb-lib/src/materializer/orderbook/{core.rs => materializer.rs} (72%) create mode 100644 kb-lib/src/materializer/perpetuals/constants.rs rename kb-lib/src/materializer/perpetuals/{core.rs => materializer.rs} (72%) create mode 100644 kb-lib/src/materializer/pool/state/constants.rs create mode 100644 kb-lib/src/materializer/pool/state/materializer.rs create mode 100644 kb-lib/src/materializer/rewards/constants.rs rename kb-lib/src/materializer/rewards/{core.rs => materializer.rs} (69%) rename kb-lib/src/materializer/risk/{core.rs => materializer.rs} (95%) create mode 100644 kb-lib/src/materializer/routing/constants.rs rename kb-lib/src/materializer/routing/{core.rs => materializer.rs} (72%) rename kb-lib/src/materializer/staking/{core.rs => materializer.rs} (92%) create mode 100644 kb-lib/src/materializer/token/accounts/constants.rs create mode 100644 kb-lib/src/materializer/token/accounts/materializer.rs delete mode 100644 kb-lib/src/materializer/token/constants.rs create mode 100644 kb-lib/src/materializer/token/metadata_risk/constants.rs create mode 100644 kb-lib/src/materializer/token/metadata_risk/materializer.rs create mode 100644 kb-lib/src/materializer/trades/constants.rs rename kb-lib/src/materializer/trades/{core.rs => materializer.rs} (69%) create mode 100644 kb-lib/src/materializer/transaction/annotations/constants.rs create mode 100644 kb-lib/src/materializer/transaction/annotations/materializer.rs delete mode 100644 kb-lib/src/materializer/transaction/constants.rs create mode 100644 kb-lib/src/materializer/vault/constants.rs rename kb-lib/src/materializer/vault/{core.rs => materializer.rs} (72%) create mode 100644 kb-lib/tests/external_solana_program_metadata_executor_api.rs create mode 100644 test-fixtures/contract-matrices/SOLANA_PROGRAM_METADATA_EXECUTOR_MATRIX.json diff --git a/Cargo.toml b/Cargo.toml index 9eb1758..6911d7e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ # file: Cargo.toml -# version: 28 +# version: 29 [workspace] resolver = "3" @@ -18,7 +18,7 @@ members = [ ] [workspace.package] -version = "0.4.8-pre.6" +version = "0.4.8-pre.7" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-bot3" diff --git a/ROADMAP.md b/ROADMAP.md index 5ad85af..3022e67 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,5 +1,5 @@ - + # ROADMAP — khadhroony-bot3 @@ -70,7 +70,8 @@ La première prerelease produit le plan vivant et l’inventaire fermé avant to - compléter multi-wallets, import/export, chiffrement, verrouillage, sauvegarde et restauration dans `kb-wallet` ; - auditer pool, résilience, administration et besoins historiques de `kb-store` ; - maintenir les scénarios UI Devnet/Testnet dans le desktop et leurs équivalents automatisés dans `kb-pipeline-demo-scenarios` ; -- compléter les validations Devnet spécialisées Metaplex Token Metadata reportées de `0.4.7` lorsque leur utilité le justifie. +- compléter les validations Devnet spécialisées Metaplex Token Metadata reportées de `0.4.7` lorsque leur utilité le justifie ; +- exécuter l’audit transversal de complétude des décodeurs, matérialisateurs et exécuteurs déjà livrés : couverture officielle, propriété unique des faits, opérations dangereuses gouvernées par `ExSafetyChecker`, opérations obsolètes constructibles marquées `#[deprecated]` et versions remplacées maintenues en decode-only. ## 0.6.x — Anchor générique, programmes SPL et Metaplex complémentaires diff --git a/config/README.md b/config/README.md index 17a3089..c4e9d77 100644 --- a/config/README.md +++ b/config/README.md @@ -1,5 +1,5 @@ - + # Configuration locale @@ -68,9 +68,12 @@ Les matérialisateurs natifs consolidés rejoignent la console et disposent de l ```text logs//kb-lib/materializer/admin/ -logs//kb-lib/materializer/compliance/ +logs//kb-lib/materializer/compliance/audit/ logs//kb-lib/materializer/lifecycle/ +logs//kb-lib/materializer/risk/ logs//kb-lib/materializer/staking/ +logs//kb-lib/materializer/token/accounts/ +logs//kb-lib/materializer/transaction/annotations/ ``` 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. diff --git a/config/example.config.json b/config/example.config.json index cbb2ce5..cb27d8f 100644 --- a/config/example.config.json +++ b/config/example.config.json @@ -584,42 +584,42 @@ ] }, { - "name": "file_kb-lib_materializer_compliance_debug", + "name": "file_kb-lib_materializer_compliance_audit_debug", "enabled": true, "sink": "file", "level": "debug", - "path": "logs/devnet/kb-lib/materializer/compliance/debug.log", + "path": "logs/devnet/kb-lib/materializer/compliance/audit/debug.log", "rotation": "daily", "format": "human", "ansi": false, "targets": [ - "kb-lib.materializer.compliance" + "kb-lib.materializer.compliance.audit" ] }, { - "name": "file_kb-lib_materializer_compliance_info", + "name": "file_kb-lib_materializer_compliance_audit_info", "enabled": true, "sink": "file", "level": "info", - "path": "logs/devnet/kb-lib/materializer/compliance/info.log", + "path": "logs/devnet/kb-lib/materializer/compliance/audit/info.log", "rotation": "daily", "format": "human", "ansi": false, "targets": [ - "kb-lib.materializer.compliance" + "kb-lib.materializer.compliance.audit" ] }, { - "name": "file_kb-lib_materializer_compliance_error", + "name": "file_kb-lib_materializer_compliance_audit_error", "enabled": true, "sink": "file", "level": "error", - "path": "logs/devnet/kb-lib/materializer/compliance/error.jsonl", + "path": "logs/devnet/kb-lib/materializer/compliance/audit/error.jsonl", "rotation": "daily", "format": "json", "ansi": false, "targets": [ - "kb-lib.materializer.compliance" + "kb-lib.materializer.compliance.audit" ] }, { @@ -662,42 +662,42 @@ ] }, { - "name": "file_kb-lib_materializer_transaction_debug", + "name": "file_kb-lib_materializer_transaction_annotations_debug", "enabled": true, "sink": "file", "level": "debug", - "path": "logs/devnet/kb-lib/materializer/transaction/debug.log", + "path": "logs/devnet/kb-lib/materializer/transaction/annotations/debug.log", "rotation": "daily", "format": "human", "ansi": false, "targets": [ - "kb-lib.materializer.transaction" + "kb-lib.materializer.transaction.annotations" ] }, { - "name": "file_kb-lib_materializer_transaction_info", + "name": "file_kb-lib_materializer_transaction_annotations_info", "enabled": true, "sink": "file", "level": "info", - "path": "logs/devnet/kb-lib/materializer/transaction/info.log", + "path": "logs/devnet/kb-lib/materializer/transaction/annotations/info.log", "rotation": "daily", "format": "human", "ansi": false, "targets": [ - "kb-lib.materializer.transaction" + "kb-lib.materializer.transaction.annotations" ] }, { - "name": "file_kb-lib_materializer_transaction_error", + "name": "file_kb-lib_materializer_transaction_annotations_error", "enabled": true, "sink": "file", "level": "error", - "path": "logs/devnet/kb-lib/materializer/transaction/error.jsonl", + "path": "logs/devnet/kb-lib/materializer/transaction/annotations/error.jsonl", "rotation": "daily", "format": "json", "ansi": false, "targets": [ - "kb-lib.materializer.transaction" + "kb-lib.materializer.transaction.annotations" ] }, { @@ -899,42 +899,42 @@ ] }, { - "name": "file_kb-lib_materializer_token_debug", + "name": "file_kb-lib_materializer_token_accounts_debug", "enabled": true, "sink": "file", "level": "debug", - "path": "logs/devnet/kb-lib/materializer/token/debug.log", + "path": "logs/devnet/kb-lib/materializer/token/accounts/debug.log", "rotation": "daily", "format": "human", "ansi": false, "targets": [ - "kb-lib.materializer.token" + "kb-lib.materializer.token.accounts" ] }, { - "name": "file_kb-lib_materializer_token_info", + "name": "file_kb-lib_materializer_token_accounts_info", "enabled": true, "sink": "file", "level": "info", - "path": "logs/devnet/kb-lib/materializer/token/info.log", + "path": "logs/devnet/kb-lib/materializer/token/accounts/info.log", "rotation": "daily", "format": "human", "ansi": false, "targets": [ - "kb-lib.materializer.token" + "kb-lib.materializer.token.accounts" ] }, { - "name": "file_kb-lib_materializer_token_error", + "name": "file_kb-lib_materializer_token_accounts_error", "enabled": true, "sink": "file", "level": "error", - "path": "logs/devnet/kb-lib/materializer/token/error.jsonl", + "path": "logs/devnet/kb-lib/materializer/token/accounts/error.jsonl", "rotation": "daily", "format": "json", "ansi": false, "targets": [ - "kb-lib.materializer.token" + "kb-lib.materializer.token.accounts" ] }, { @@ -2002,42 +2002,42 @@ ] }, { - "name": "file_kb-lib_materializer_compliance_debug", + "name": "file_kb-lib_materializer_compliance_audit_debug", "enabled": true, "sink": "file", "level": "debug", - "path": "logs/mainnet_research/kb-lib/materializer/compliance/debug.log", + "path": "logs/mainnet_research/kb-lib/materializer/compliance/audit/debug.log", "rotation": "daily", "format": "human", "ansi": false, "targets": [ - "kb-lib.materializer.compliance" + "kb-lib.materializer.compliance.audit" ] }, { - "name": "file_kb-lib_materializer_compliance_info", + "name": "file_kb-lib_materializer_compliance_audit_info", "enabled": true, "sink": "file", "level": "info", - "path": "logs/mainnet_research/kb-lib/materializer/compliance/info.log", + "path": "logs/mainnet_research/kb-lib/materializer/compliance/audit/info.log", "rotation": "daily", "format": "human", "ansi": false, "targets": [ - "kb-lib.materializer.compliance" + "kb-lib.materializer.compliance.audit" ] }, { - "name": "file_kb-lib_materializer_compliance_error", + "name": "file_kb-lib_materializer_compliance_audit_error", "enabled": true, "sink": "file", "level": "error", - "path": "logs/mainnet_research/kb-lib/materializer/compliance/error.jsonl", + "path": "logs/mainnet_research/kb-lib/materializer/compliance/audit/error.jsonl", "rotation": "daily", "format": "json", "ansi": false, "targets": [ - "kb-lib.materializer.compliance" + "kb-lib.materializer.compliance.audit" ] }, { @@ -2080,42 +2080,42 @@ ] }, { - "name": "file_kb-lib_materializer_transaction_debug", + "name": "file_kb-lib_materializer_transaction_annotations_debug", "enabled": true, "sink": "file", "level": "debug", - "path": "logs/mainnet_research/kb-lib/materializer/transaction/debug.log", + "path": "logs/mainnet_research/kb-lib/materializer/transaction/annotations/debug.log", "rotation": "daily", "format": "human", "ansi": false, "targets": [ - "kb-lib.materializer.transaction" + "kb-lib.materializer.transaction.annotations" ] }, { - "name": "file_kb-lib_materializer_transaction_info", + "name": "file_kb-lib_materializer_transaction_annotations_info", "enabled": true, "sink": "file", "level": "info", - "path": "logs/mainnet_research/kb-lib/materializer/transaction/info.log", + "path": "logs/mainnet_research/kb-lib/materializer/transaction/annotations/info.log", "rotation": "daily", "format": "human", "ansi": false, "targets": [ - "kb-lib.materializer.transaction" + "kb-lib.materializer.transaction.annotations" ] }, { - "name": "file_kb-lib_materializer_transaction_error", + "name": "file_kb-lib_materializer_transaction_annotations_error", "enabled": true, "sink": "file", "level": "error", - "path": "logs/mainnet_research/kb-lib/materializer/transaction/error.jsonl", + "path": "logs/mainnet_research/kb-lib/materializer/transaction/annotations/error.jsonl", "rotation": "daily", "format": "json", "ansi": false, "targets": [ - "kb-lib.materializer.transaction" + "kb-lib.materializer.transaction.annotations" ] }, { @@ -2317,42 +2317,42 @@ ] }, { - "name": "file_kb-lib_materializer_token_debug", + "name": "file_kb-lib_materializer_token_accounts_debug", "enabled": true, "sink": "file", "level": "debug", - "path": "logs/mainnet_research/kb-lib/materializer/token/debug.log", + "path": "logs/mainnet_research/kb-lib/materializer/token/accounts/debug.log", "rotation": "daily", "format": "human", "ansi": false, "targets": [ - "kb-lib.materializer.token" + "kb-lib.materializer.token.accounts" ] }, { - "name": "file_kb-lib_materializer_token_info", + "name": "file_kb-lib_materializer_token_accounts_info", "enabled": true, "sink": "file", "level": "info", - "path": "logs/mainnet_research/kb-lib/materializer/token/info.log", + "path": "logs/mainnet_research/kb-lib/materializer/token/accounts/info.log", "rotation": "daily", "format": "human", "ansi": false, "targets": [ - "kb-lib.materializer.token" + "kb-lib.materializer.token.accounts" ] }, { - "name": "file_kb-lib_materializer_token_error", + "name": "file_kb-lib_materializer_token_accounts_error", "enabled": true, "sink": "file", "level": "error", - "path": "logs/mainnet_research/kb-lib/materializer/token/error.jsonl", + "path": "logs/mainnet_research/kb-lib/materializer/token/accounts/error.jsonl", "rotation": "daily", "format": "json", "ansi": false, "targets": [ - "kb-lib.materializer.token" + "kb-lib.materializer.token.accounts" ] }, { @@ -3620,42 +3620,42 @@ ] }, { - "name": "file_kb-lib_materializer_compliance_debug", + "name": "file_kb-lib_materializer_compliance_audit_debug", "enabled": true, "sink": "file", "level": "debug", - "path": "logs/mainnet/kb-lib/materializer/compliance/debug.log", + "path": "logs/mainnet/kb-lib/materializer/compliance/audit/debug.log", "rotation": "daily", "format": "human", "ansi": false, "targets": [ - "kb-lib.materializer.compliance" + "kb-lib.materializer.compliance.audit" ] }, { - "name": "file_kb-lib_materializer_compliance_info", + "name": "file_kb-lib_materializer_compliance_audit_info", "enabled": true, "sink": "file", "level": "info", - "path": "logs/mainnet/kb-lib/materializer/compliance/info.log", + "path": "logs/mainnet/kb-lib/materializer/compliance/audit/info.log", "rotation": "daily", "format": "human", "ansi": false, "targets": [ - "kb-lib.materializer.compliance" + "kb-lib.materializer.compliance.audit" ] }, { - "name": "file_kb-lib_materializer_compliance_error", + "name": "file_kb-lib_materializer_compliance_audit_error", "enabled": true, "sink": "file", "level": "error", - "path": "logs/mainnet/kb-lib/materializer/compliance/error.jsonl", + "path": "logs/mainnet/kb-lib/materializer/compliance/audit/error.jsonl", "rotation": "daily", "format": "json", "ansi": false, "targets": [ - "kb-lib.materializer.compliance" + "kb-lib.materializer.compliance.audit" ] }, { @@ -3698,42 +3698,42 @@ ] }, { - "name": "file_kb-lib_materializer_transaction_debug", + "name": "file_kb-lib_materializer_transaction_annotations_debug", "enabled": true, "sink": "file", "level": "debug", - "path": "logs/mainnet/kb-lib/materializer/transaction/debug.log", + "path": "logs/mainnet/kb-lib/materializer/transaction/annotations/debug.log", "rotation": "daily", "format": "human", "ansi": false, "targets": [ - "kb-lib.materializer.transaction" + "kb-lib.materializer.transaction.annotations" ] }, { - "name": "file_kb-lib_materializer_transaction_info", + "name": "file_kb-lib_materializer_transaction_annotations_info", "enabled": true, "sink": "file", "level": "info", - "path": "logs/mainnet/kb-lib/materializer/transaction/info.log", + "path": "logs/mainnet/kb-lib/materializer/transaction/annotations/info.log", "rotation": "daily", "format": "human", "ansi": false, "targets": [ - "kb-lib.materializer.transaction" + "kb-lib.materializer.transaction.annotations" ] }, { - "name": "file_kb-lib_materializer_transaction_error", + "name": "file_kb-lib_materializer_transaction_annotations_error", "enabled": true, "sink": "file", "level": "error", - "path": "logs/mainnet/kb-lib/materializer/transaction/error.jsonl", + "path": "logs/mainnet/kb-lib/materializer/transaction/annotations/error.jsonl", "rotation": "daily", "format": "json", "ansi": false, "targets": [ - "kb-lib.materializer.transaction" + "kb-lib.materializer.transaction.annotations" ] }, { @@ -3935,42 +3935,42 @@ ] }, { - "name": "file_kb-lib_materializer_token_debug", + "name": "file_kb-lib_materializer_token_accounts_debug", "enabled": true, "sink": "file", "level": "debug", - "path": "logs/mainnet/kb-lib/materializer/token/debug.log", + "path": "logs/mainnet/kb-lib/materializer/token/accounts/debug.log", "rotation": "daily", "format": "human", "ansi": false, "targets": [ - "kb-lib.materializer.token" + "kb-lib.materializer.token.accounts" ] }, { - "name": "file_kb-lib_materializer_token_info", + "name": "file_kb-lib_materializer_token_accounts_info", "enabled": true, "sink": "file", "level": "info", - "path": "logs/mainnet/kb-lib/materializer/token/info.log", + "path": "logs/mainnet/kb-lib/materializer/token/accounts/info.log", "rotation": "daily", "format": "human", "ansi": false, "targets": [ - "kb-lib.materializer.token" + "kb-lib.materializer.token.accounts" ] }, { - "name": "file_kb-lib_materializer_token_error", + "name": "file_kb-lib_materializer_token_accounts_error", "enabled": true, "sink": "file", "level": "error", - "path": "logs/mainnet/kb-lib/materializer/token/error.jsonl", + "path": "logs/mainnet/kb-lib/materializer/token/accounts/error.jsonl", "rotation": "daily", "format": "json", "ansi": false, "targets": [ - "kb-lib.materializer.token" + "kb-lib.materializer.token.accounts" ] }, { diff --git a/docs/OPERATION_NAMING_CONVENTION.md b/docs/OPERATION_NAMING_CONVENTION.md index 14848f4..019f5e9 100644 --- a/docs/OPERATION_NAMING_CONVENTION.md +++ b/docs/OPERATION_NAMING_CONVENTION.md @@ -1,5 +1,5 @@ - + # Convention canonique des identités runtime et des codes d’opération @@ -82,6 +82,15 @@ kb-lib.materializer.transaction.annotations Cette identité désigne le composant logiciel, pas une opération on-chain. +Pour un matérialisateur, le tracing target reprend exactement cette identité runtime. La paire avec le `processor_name` est déterministe : + +```text +component_name = kb-lib. +processor_name = component_name sans le préfixe kb-lib. +``` + +Les domaines spécialisés ne doivent pas être raccourcis : utiliser `compliance.audit`, `transaction.annotations` et `token.accounts`, et non les parents génériques `compliance`, `transaction` ou `token`. + ### 3.2 `processor_name` Format : @@ -265,4 +274,3 @@ Avant de fusionner une nouvelle opération : 6. ajouter ou mettre à jour la matrice normative ; 7. ajouter les tests de cohérence entre constantes et matrice ; 8. vérifier l’impact PostgreSQL avant tout backfill ou replay durable. - diff --git a/docs/README.md b/docs/README.md index 01e3dce..5ce349a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,5 +1,5 @@ - + # Documentation active de Khadhroony Bot3 @@ -40,6 +40,8 @@ Modèles documentaires non génératifs : - [`audits/V0_4_7_PRE_016_DOCUMENTATION_AND_HEADERS_AUDIT.md`](audits/V0_4_7_PRE_016_DOCUMENTATION_AND_HEADERS_AUDIT.md) ; - [`audits/V0_4_8_PRE_002_METADATA_CONTRACT_AUDIT.md`](audits/V0_4_8_PRE_002_METADATA_CONTRACT_AUDIT.md) ; +- [`audits/V0_4_8_PRE_007_METAPLEX_MATERIALIZER_STRUCTURE_AUDIT.md`](audits/V0_4_8_PRE_007_METAPLEX_MATERIALIZER_STRUCTURE_AUDIT.md) ; +- [`audits/V0_4_8_PRE_007_MATERIALIZER_CONVENTION_AUDIT.md`](audits/V0_4_8_PRE_007_MATERIALIZER_CONVENTION_AUDIT.md) ; - [`decisions/DOCUMENT_ARCHIVE_SELECTION_POLICY.md`](decisions/DOCUMENT_ARCHIVE_SELECTION_POLICY.md) ; - [`decisions/WINCODE_COMPATIBILITY_POLICY.md`](decisions/WINCODE_COMPATIBILITY_POLICY.md). @@ -97,7 +99,9 @@ Les onze crates possèdent désormais `README.md`, `TODO.md`, `USAGE.md` et `CHA ## Audits actifs - [`Audit contractuel 0.4.8-pre.002 — metadata`](audits/V0_4_8_PRE_002_METADATA_CONTRACT_AUDIT.md) ; -- [`Audit 0.4.8-pre.005 — instructions et historique Solana Program Metadata`](audits/V0_4_8_PRE_005_SOLANA_PROGRAM_METADATA_INSTRUCTION_HISTORY_AUDIT.md). +- [`Audit 0.4.8-pre.005 — instructions et historique Solana Program Metadata`](audits/V0_4_8_PRE_005_SOLANA_PROGRAM_METADATA_INSTRUCTION_HISTORY_AUDIT.md) ; +- [`Audit 0.4.8-pre.007 — structure du matérialiseur Metaplex Token Metadata`](audits/V0_4_8_PRE_007_METAPLEX_MATERIALIZER_STRUCTURE_AUDIT.md) ; +- [`Audit 0.4.8-pre.007 — conventions et structure de tous les matérialisateurs`](audits/V0_4_8_PRE_007_MATERIALIZER_CONVENTION_AUDIT.md). ## Plans de version actifs diff --git a/docs/audits/V0_4_8_PRE_002_METADATA_CONTRACT_AUDIT.md b/docs/audits/V0_4_8_PRE_002_METADATA_CONTRACT_AUDIT.md index 0f53f94..3f3409f 100644 --- a/docs/audits/V0_4_8_PRE_002_METADATA_CONTRACT_AUDIT.md +++ b/docs/audits/V0_4_8_PRE_002_METADATA_CONTRACT_AUDIT.md @@ -1,5 +1,5 @@ - + # Audit contractuel `0.4.8-pre.002` — Solana Program Metadata et Token-2022 @@ -48,11 +48,11 @@ La structure, le Program ID, la version déclarée, les PDA, les comptes et les ### 4.1 PDA -| PDA | Seeds | -|---|---| -| canonical | `[program, seed]` | -| non-canonical | `[program, authority, seed]` | -| metadata | helper conditionnel sélectionnant l’une des deux dérivations selon la présence d’une autorité tierce | +| PDA | Seeds | +|---------------|------------------------------------------------------------------------------------------------------| +| canonical | `[program, seed]` | +| non-canonical | `[program, authority, seed]` | +| metadata | helper conditionnel sélectionnant l’une des deux dérivations selon la présence d’une autorité tierce | `seed` est une chaîne UTF-8 de taille fixe 16 octets dans l’IDL. Le code doit distinguer longueur en octets et nombre de caractères Unicode. @@ -76,27 +76,27 @@ Ces valeurs décrivent uniquement les données on-chain. `Url` ne déclenche auc ### 4.4 Instructions -| Discriminant | Instruction | Comptes | Arguments | -|---:|---|---|---| -| `0` | `write` | `buffer` (writable)
`authority` (signer)
`sourceBuffer` (optional) | `offset: u32`
`data: Option` | -| `1` | `initialize` | `metadata` (writable)
`authority` (signer)
`program`
`programData` (optional)
`system` (optional) | `seed: Seed`
`encoding: Encoding`
`compression: Compression`
`format: Format`
`dataSource: DataSource`
`data: Option` | -| `2` | `setAuthority` | `account` (writable)
`authority` (signer)
`program` (optional)
`programData` (optional) | `newAuthority: Option` | -| `3` | `setData` | `metadata` (writable)
`authority` (signer)
`buffer` (writable, optional)
`program` (optional)
`programData` (optional) | `encoding: Encoding`
`compression: Compression`
`format: Format`
`dataSource: DataSource`
`data: Option` | -| `4` | `setImmutable` | `metadata` (writable)
`authority` (signer)
`program` (optional)
`programData` (optional) | — | -| `5` | `trim` | `account` (writable)
`authority` (signer)
`program` (optional)
`programData` (optional)
`destination` (writable)
`rent` | — | -| `6` | `close` | `account` (writable)
`authority` (signer)
`program` (optional)
`programData` (optional)
`destination` (writable) | — | -| `7` | `allocate` | `buffer` (writable)
`authority` (signer)
`program` (optional)
`programData` (optional)
`system` (optional) | `seed: Option` | -| `8` | `extend` | `account` (writable)
`authority` (signer)
`program` (optional)
`programData` (optional) | `length: u16` | +| Discriminant | Instruction | Comptes | Arguments | +|-------------:|----------------|----------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------| +| `0` | `write` | `buffer` (writable)
`authority` (signer)
`sourceBuffer` (optional) | `offset: u32`
`data: Option` | +| `1` | `initialize` | `metadata` (writable)
`authority` (signer)
`program`
`programData` (optional)
`system` (optional) | `seed: Seed`
`encoding: Encoding`
`compression: Compression`
`format: Format`
`dataSource: DataSource`
`data: Option` | +| `2` | `setAuthority` | `account` (writable)
`authority` (signer)
`program` (optional)
`programData` (optional) | `newAuthority: Option` | +| `3` | `setData` | `metadata` (writable)
`authority` (signer)
`buffer` (writable, optional)
`program` (optional)
`programData` (optional) | `encoding: Encoding`
`compression: Compression`
`format: Format`
`dataSource: DataSource`
`data: Option` | +| `4` | `setImmutable` | `metadata` (writable)
`authority` (signer)
`program` (optional)
`programData` (optional) | — | +| `5` | `trim` | `account` (writable)
`authority` (signer)
`program` (optional)
`programData` (optional)
`destination` (writable)
`rent` | — | +| `6` | `close` | `account` (writable)
`authority` (signer)
`program` (optional)
`programData` (optional)
`destination` (writable) | — | +| `7` | `allocate` | `buffer` (writable)
`authority` (signer)
`program` (optional)
`programData` (optional)
`system` (optional) | `seed: Option` | +| `8` | `extend` | `account` (writable)
`authority` (signer)
`program` (optional)
`programData` (optional) | `length: u16` | ### 4.5 Erreurs officielles -| Code | Erreur | -|---:|---| -| 0 | `NotExecutableAccount` | -| 1 | `InvalidProgramState` | -| 2 | `InvalidProgramDataAccount` | -| 3 | `ImmutableMetadataAccount` | -| 4 | `InvalidDataLength` | +| Code | Erreur | +|-----:|-----------------------------| +| 0 | `NotExecutableAccount` | +| 1 | `InvalidProgramState` | +| 2 | `InvalidProgramDataAccount` | +| 3 | `ImmutableMetadataAccount` | +| 4 | `InvalidDataLength` | ## 5. Décisions de modèles et de validation `ProgM6…` @@ -113,13 +113,13 @@ Ces valeurs décrivent uniquement les données on-chain. `Url` ne déclenche auc ## 6. Matrice de couverture Token-2022 réelle -| Capacité | Décodage wire | Intent public | Builder | Exécution | Lecture stateful | Matérialisation | Scénario dédié | Desktop | Devnet confirmé | -|---|---|---|---|---|---|---|---|---|---| -| `Initialize` | oui | oui | oui | oui, via builder générique | état extension lisible | extension observée | non dédié | non dédié | non établi | -| `UpdateField` | oui | oui | oui | oui, via builder générique | état extension lisible | extension observée | non dédié | non dédié | non établi | -| `RemoveKey` | oui | oui | oui | oui, via builder générique | état extension lisible | extension observée | non dédié | non dédié | non établi | -| `UpdateAuthority` | oui | non | non | non | état extension lisible | extension observée | non | non | non | -| `Emit` | oui | non | non | non | résultat de retour non orchestré | non dédiée | non | non | non | +| Capacité | Décodage wire | Intent public | Builder | Exécution | Lecture stateful | Matérialisation | Scénario dédié | Desktop | Devnet confirmé | +|-------------------|---------------|---------------|---------|----------------------------|----------------------------------|--------------------|----------------|-----------|-----------------| +| `Initialize` | oui | oui | oui | oui, via builder générique | état extension lisible | extension observée | non dédié | non dédié | non établi | +| `UpdateField` | oui | oui | oui | oui, via builder générique | état extension lisible | extension observée | non dédié | non dédié | non établi | +| `RemoveKey` | oui | oui | oui | oui, via builder générique | état extension lisible | extension observée | non dédié | non dédié | non établi | +| `UpdateAuthority` | oui | non | non | non | état extension lisible | extension observée | non | non | non | +| `Emit` | oui | non | non | non | résultat de retour non orchestré | non dédiée | non | non | non | Constats complémentaires : diff --git a/docs/audits/V0_4_8_PRE_007_MATERIALIZER_CONVENTION_AUDIT.md b/docs/audits/V0_4_8_PRE_007_MATERIALIZER_CONVENTION_AUDIT.md new file mode 100644 index 0000000..47fc975 --- /dev/null +++ b/docs/audits/V0_4_8_PRE_007_MATERIALIZER_CONVENTION_AUDIT.md @@ -0,0 +1,189 @@ + + + +# Audit des conventions et de la structure des matérialisateurs — `0.4.8-pre.007-fix-004` + +## 1. Objet + +Cet audit couvre l’intégralité de `kb-lib/src/materializer` après l’activation du matérialisateur Solana Program Metadata et l’extraction des projections Metaplex Token Metadata et Token-2022. + +Les contrôles portent sur : + +- les identités runtime et les `processorName` persistés ; +- les tracing targets ; +- la structure des façades et sous-modules ; +- l’emplacement, le nommage et la réexportation des constantes ; +- les contrats communs `MtMaterializer` et `MtApiEventMaterializer` ; +- la neutralité effective des matérialisateurs encore réservés ; +- la provenance et les versions de projection des sorties actives. + +## 2. Convention retenue + +Chaque composant possède une paire d’identités issue de son `constants.rs` : + +```text +runtime component : kb-lib.materializer.[.] +processorName : materializer.[.] +``` + +Le `processorName` est exactement l’identité runtime privée du préfixe `kb-lib.`. Le tracing target d’un composant actif est identique à son identité runtime. + +Exemples : + +```text +kb-lib.materializer.compliance.audit +materializer.compliance.audit + +kb-lib.materializer.transaction.annotations +materializer.transaction.annotations + +kb-lib.materializer.metadata.solana_program_metadata +materializer.metadata.solana_program_metadata +``` + +Les identités runtime servent au code et au routage des logs. Les `processorName` servent aux sorties persistées, au replay et à la provenance. Ils ne doivent pas être interchangeables. + +## 3. Inventaire audité + +| Composant | Type principal ou API | Statut | Identité runtime | `processorName` | +|-------------------------|---------------------------------------|------------|--------------------------------------------------------|-------------------------------------------------| +| Administration | `MtAdminMaterializer` | actif | `kb-lib.materializer.admin` | `materializer.admin` | +| Bridge | `MtBridgeMaterializer` | réservé | `kb-lib.materializer.bridge` | `materializer.bridge` | +| Compliance audit | `MtComplianceAuditMaterializer` | actif | `kb-lib.materializer.compliance.audit` | `materializer.compliance.audit` | +| Fees | `MtFeesMaterializer` | actif | `kb-lib.materializer.fees` | `materializer.fees` | +| Governance | `MtGovernanceMaterializer` | réservé | `kb-lib.materializer.governance` | `materializer.governance` | +| Lending | `MtLendingMaterializer` | réservé | `kb-lib.materializer.lending` | `materializer.lending` | +| Lifecycle | `MtLifecycleMaterializer` | actif | `kb-lib.materializer.lifecycle` | `materializer.lifecycle` | +| Liquidity | `MtLiquidityMaterializer` | réservé | `kb-lib.materializer.liquidity` | `materializer.liquidity` | +| Metaplex Token Metadata | APIs de snapshots | state-only | `kb-lib.materializer.metadata.metaplex_token_metadata` | `materializer.metadata.metaplex_token_metadata` | +| Solana Program Metadata | `MtSolanaProgramMetadataMaterializer` | actif | `kb-lib.materializer.metadata.solana_program_metadata` | `materializer.metadata.solana_program_metadata` | +| Token-2022 metadata | API de snapshots | state-only | `kb-lib.materializer.metadata.token_2022` | `materializer.metadata.token_2022` | +| NFT | `MtNftMaterializer` | réservé | `kb-lib.materializer.nft` | `materializer.nft` | +| Oracle | `MtOracleMaterializer` | réservé | `kb-lib.materializer.oracle` | `materializer.oracle` | +| Orderbook | `MtOrderbookMaterializer` | réservé | `kb-lib.materializer.orderbook` | `materializer.orderbook` | +| Perpetuals | `MtPerpetualsMaterializer` | réservé | `kb-lib.materializer.perpetuals` | `materializer.perpetuals` | +| Pool state | `MtPoolStateMaterializer` | réservé | `kb-lib.materializer.pool.state` | `materializer.pool.state` | +| Rewards | `MtRewardsMaterializer` | réservé | `kb-lib.materializer.rewards` | `materializer.rewards` | +| Risk | `MtRiskMaterializer` | actif | `kb-lib.materializer.risk` | `materializer.risk` | +| Routing | `MtRoutingMaterializer` | réservé | `kb-lib.materializer.routing` | `materializer.routing` | +| Staking | `MtStakingMaterializer` | actif | `kb-lib.materializer.staking` | `materializer.staking` | +| Token accounts | `MtTokenAccountsMaterializer` | actif | `kb-lib.materializer.token.accounts` | `materializer.token.accounts` | +| Token metadata risk | `MtTokenMetadataRiskMaterializer` | réservé | `kb-lib.materializer.token.metadata_risk` | `materializer.token.metadata_risk` | +| Trades | `MtTradesMaterializer` | réservé | `kb-lib.materializer.trades` | `materializer.trades` | +| Transaction annotations | `MtTransactionAnnotationMaterializer` | actif | `kb-lib.materializer.transaction.annotations` | `materializer.transaction.annotations` | +| Vault | `MtVaultMaterializer` | réservé | `kb-lib.materializer.vault` | `materializer.vault` | + +Bilan : **25 composants nommés**, dont **9 matérialisateurs actifs**, **14 coquilles réservées** et **2 composants state-only**. + +`MtMetadataMaterializer` reste un wrapper public de compatibilité vers `MtSolanaProgramMetadataMaterializer`. Il ne possède pas une identité distincte et ne crée pas un second propriétaire de projection. + +## 4. Structure normalisée + +Un matérialisateur simple suit désormais la structure : + +```text +.rs +/constants.rs +/materializer.rs +``` + +La façade `.rs` ne contient que les déclarations de sous-modules et leurs réexports. Les composants complexes peuvent ajouter des fichiers spécialisés, par exemple `state.rs` ou `account.rs`, sans placer d’implémentation dans la façade. + +Les domaines imbriqués suivent la même règle : + +```text +compliance/audit.rs +compliance/audit/constants.rs +compliance/audit/materializer.rs + +transaction/annotations.rs +transaction/annotations/constants.rs +transaction/annotations/materializer.rs +``` + +Les anciens noms génériques `core.rs` ont été remplacés par `materializer.rs`. Les tables de surfaces, familles acceptées, identités, versions de projection et tracing targets résident dans les `constants.rs`, sont réexportées jusqu’à `kb-lib/src/lib.rs` et sont consommées via `crate::`. + +## 5. Coquilles réservées + +Une coquille réservée doit : + +- conserver son type public `Mt*Materializer` ; +- implémenter les deux traits communs ; +- exposer une constante `MT_*_ACCEPTED_FAMILIES` vide ; +- retourner `false` depuis l’ancien `accepts_event` ; +- retourner une liste vide depuis l’ancien `materialize_event` ; +- retourner `Ignored` depuis le contrat contextuel ; +- ne produire aucun événement, aucune sortie et aucune provenance fictive. + +Le comportement antérieur qui pouvait retourner un événement legacy malgré une frontière inactive a été supprimé. + +## 6. Provenance et versionnement + +Les sorties actives utilisent uniquement leur constante `MT_*_PROCESSOR_NAME`. Aucun `processorName` littéral ne reste dans les implémentations. + +Les changements de provenance Metaplex et Token-2022 livrés dans `fix-002` restent associés à `projectionVersion = 2`. Solana Program Metadata conserve `projectionVersion = 1`, car sa projection est introduite dans `0.4.8`. + +L’ajout d’une provenance explicite aux sorties qui ne possédaient encore qu’une version numérique modifie leur contrat persisté. Les matérialisateurs `admin`, `compliance.audit`, `fees`, `lifecycle` et `staking` passent donc de `projectionVersion = 1` à `projectionVersion = 2`. Les matérialisateurs `risk`, `token.accounts` et `transaction.annotations` conservaient déjà une provenance spécialisée et gardent leurs versions existantes. + +Les `output_key` et les familles métier existantes ne sont pas renommées par cet audit. + +## 7. Configuration de logging + +Les routes de `config/example.config.json` sont alignées sur les tracing targets spécialisés : + +```text +kb-lib.materializer.compliance.audit +kb-lib.materializer.transaction.annotations +kb-lib.materializer.token.accounts +``` + +Les noms de sinks et leurs chemins sont harmonisés avec les mêmes sous-domaines. + +## 8. Garde-fous ajoutés + +L’audit mécanique du workspace vérifie désormais : + +- la paire obligatoire `MT_*_COMPONENT_NAME` / `MT_*_PROCESSOR_NAME` ; +- la relation exacte obtenue par suppression du préfixe `kb-lib.` ; +- l’absence de constantes hors `constants.rs` ; +- la présence d’un `constants.rs` à côté de chaque implémentation ; +- l’absence d’identités littérales dans les implémentations ; +- la présence conjointe de `projectionVersion` et `processorName` dans chaque payload versionné ; +- l’utilisation obligatoire des constantes `MT_*_PROJECTION_VERSION` et `MT_*_PROCESSOR_NAME` dans ces payloads ; +- la pureté des façades ; +- le résultat vide des deux contrats d’une coquille réservée. + +Les tests Rust conservent en plus l’inventaire typé des matérialisateurs et contrôlent la convention de nommage. + +## 9. Fichiers obsolètes à supprimer + +Les 22 chemins retirés sont fournis dans `delete-files.txt` à la racine du delta. Leur suppression est obligatoire avant la validation, car une extraction ZIP ne peut pas retirer les anciens fichiers. + +## 10. Conclusion + +Les matérialisateurs utilisent désormais une convention unique pour les identités runtime, les processeurs persistés, les tracing targets, les constantes, les façades et les coquilles réservées. Les différences restantes sont fonctionnelles et explicites : un composant peut être actif, réservé ou state-only, mais il ne change pas de convention structurelle ou de provenance selon son statut. + +## 11. Correction des identités opérationnelles et du tracing + +La compilation du workspace corrigé a révélé trois tracing targets déclarés mais sans événement réel : + +- `kb-lib.materializer.fees` ; +- `kb-lib.materializer.metadata.metaplex_token_metadata` ; +- `kb-lib.materializer.metadata.token_2022`. + +Le composant state-only Token-2022 réexportait également `MT_METADATA_TOKEN_2022_COMPONENT_NAME` sans le consommer dans son implémentation. Cette situation ne signifiait pas que les snapshots Token-2022 n’étaient pas matérialisés : la fonction `materializer_metadata_materialize_token_2022_snapshot` produisait déjà les projections `TokenMetadata`, `TokenGroup` et `TokenGroupMember`. Elle révélait en revanche que l’identité runtime du composant n’était pas attachée à une opération observable. + +Le correctif ajoute des événements structurés réels pour les chemins suivants : + +- observation et snapshots de frais Token-2022 ; +- snapshots de comptes Metaplex et `MetadataV1` ; +- snapshots metadata, group et group-member Token-2022. + +Les événements portent le tracing target du composant, son `component_name`, son `processor_name`, sa version, le statut et les identités disponibles. Aucun faux `let _target` n’est utilisé et aucun champ persistant n’est ajouté uniquement pour supprimer un warning. + +L’audit mécanique refuse désormais : + +- un `TRACING_TARGET_*` de `kb-lib` déclaré sans macro `tracing` réelle ; +- un `MT_*_COMPONENT_NAME` qui n’est consommé par aucun fichier d’implémentation du composant. + +Les composants state-only restent distincts des matérialiseurs instructionnels : ils n’implémentent pas artificiellement `MtApiEventMaterializer`, mais leur identité runtime est désormais effectivement utilisée par leurs opérations de snapshot. diff --git a/docs/audits/V0_4_8_PRE_007_METAPLEX_MATERIALIZER_STRUCTURE_AUDIT.md b/docs/audits/V0_4_8_PRE_007_METAPLEX_MATERIALIZER_STRUCTURE_AUDIT.md new file mode 100644 index 0000000..f421d03 --- /dev/null +++ b/docs/audits/V0_4_8_PRE_007_METAPLEX_MATERIALIZER_STRUCTURE_AUDIT.md @@ -0,0 +1,109 @@ + + + +# Audit `0.4.8-pre.007` — structure et identités des matérialiseurs metadata + +## 1. Questions vérifiées + +Deux ambiguïtés structurelles ont été vérifiées : + +- l’absence initiale d’un sous-module dédié à Metaplex Token Metadata ; +- l’emplacement et la couverture de la matérialisation Token-2022 Token Metadata. + +L’audit a également séparé l’identité runtime d’un composant de son `processorName` persisté. + +## 2. Metaplex Token Metadata + +Il n’existait pas d’oubli fonctionnel en `0.4.7`. Les APIs publiques suivantes étaient déjà implémentées : + +- `materializer_metadata_materialize_metaplex_metadata_snapshot` pour l’état autoritatif `MetadataV1` ; +- `materializer_metadata_materialize_metaplex_account_snapshot` pour les autres comptes Metaplex canoniques matérialisables. + +Le défaut était structurel : ces fonctions partageaient auparavant `metadata/core.rs` avec d’autres domaines. Elles sont désormais isolées dans : + +```text +kb-lib/src/materializer/metadata/metaplex_token_metadata.rs +kb-lib/src/materializer/metadata/metaplex_token_metadata/constants.rs +kb-lib/src/materializer/metadata/metaplex_token_metadata/state.rs +kb-lib/src/materializer/metadata/metaplex_token_metadata/account.rs +``` + +Les clés de sortie et la sémantique métier restent inchangées. La provenance utilise désormais le `processorName` dédié : + +```text +materializer.metadata.metaplex_token_metadata +``` + +Comme cette valeur persistée change par rapport au contrat antérieur, `projectionVersion` passe de `1` à `2`. + +## 3. Token-2022 Token Metadata + +Les états Token-2022 étaient déjà matérialisés par : + +```text +materializer_metadata_materialize_token_2022_snapshot +``` + +Cette fonction projette les extensions de mint suivantes : + +- `TokenMetadata` ; +- `TokenGroup` ; +- `TokenGroupMember`. + +Elle était cependant placée dans `metadata/core.rs`. Le correctif l’extrait vers : + +```text +kb-lib/src/materializer/metadata/token_2022.rs +kb-lib/src/materializer/metadata/token_2022/constants.rs +kb-lib/src/materializer/metadata/token_2022/state.rs +``` + +La fonction publique et les `output_key` restent inchangées. Sa provenance utilise : + +```text +materializer.metadata.token_2022 +``` + +Le schéma de projection passe à `2` pour refléter cette correction d’identité. + +## 4. Écart fonctionnel Token-2022 restant + +La matérialisation d’état ne couvre pas à elle seule toutes les observations décodées. + +Les faits `InitializeMetadataPointer` et `UpdateMetadataPointer` sont déjà possédés par le matérialiseur d’administration. En revanche, les cinq observations de `spl-token-metadata-interface` ne disposent pas encore d’une matérialisation d’instruction dédiée : + +- `Initialize` ; +- `UpdateField` ; +- `RemoveKey` ; +- `UpdateAuthority` ; +- `Emit`. + +Cet écart doit être fermé dans `0.4.8-pre.010`, en même temps que la complétude des intents, builders, lectures avant/après et postconditions Token-2022. + +## 5. Solana Program Metadata + +L’identité runtime du composant reste : + +```text +kb-lib.materializer.metadata.solana_program_metadata +``` + +Le `processorName` persistant du composant Solana Program Metadata est corrigé en : + +```text +materializer.metadata.solana_program_metadata +``` + +Le tracing target reste distinct et conserve son préfixe runtime : + +```text +kb-lib.materializer.metadata.solana_program_metadata +``` + +## 6. Conséquence de migration + +Les projections dérivées Metaplex et Token-2022 produites avec les anciennes provenances doivent être reconstruites depuis les raws ou migrées si elles sont conservées. Les clés de sortie ne changent pas. + +## 7. Conclusion + +Metaplex et les états metadata Token-2022 étaient déjà matérialisés. `pre.007-fix-002` corrige leurs identités et leur organisation modulaire. `pre.007-fix-003` étend ensuite la même convention à l’intégralité des matérialisateurs ; voir `V0_4_8_PRE_007_MATERIALIZER_CONVENTION_AUDIT.md`. La seule lacune fonctionnelle confirmée concerne les cinq faits d’instructions `spl-token-metadata-interface`, explicitement reportés à `pre.010`. diff --git a/docs/guides/LOGGING.md b/docs/guides/LOGGING.md index bf93ec1..0896c1c 100644 --- a/docs/guides/LOGGING.md +++ b/docs/guides/LOGGING.md @@ -1,5 +1,5 @@ - + # Guide de logging et tracing @@ -50,8 +50,13 @@ kb-pipeline.backfill kb-pipeline.decode-replay kb-onchain-transport.http kb-lib.executor.spl.token-2022 +kb-lib.materializer.compliance.audit +kb-lib.materializer.token.accounts +kb-lib.materializer.transaction.annotations ``` +Pour un matérialisateur, la target de tracing est l’identité runtime `kb-lib.materializer.[.]`. Elle reste distincte du `processorName` persisté `materializer.[.]`, qui ne doit pas être utilisé comme target de logs. + Une nouvelle target doit être ajoutée selon `docs/OPERATION_NAMING_CONVENTION.md` et les règles Khadhroony. ## Frontend desktop diff --git a/docs/plans/V0_4_8_SOLANA_PROGRAM_METADATA_AND_TOKEN_2022_COMPLETENESS_PLAN.md b/docs/plans/V0_4_8_SOLANA_PROGRAM_METADATA_AND_TOKEN_2022_COMPLETENESS_PLAN.md index 3e37289..1420e86 100644 --- a/docs/plans/V0_4_8_SOLANA_PROGRAM_METADATA_AND_TOKEN_2022_COMPLETENESS_PLAN.md +++ b/docs/plans/V0_4_8_SOLANA_PROGRAM_METADATA_AND_TOKEN_2022_COMPLETENESS_PLAN.md @@ -1,11 +1,11 @@ - + # Plan `0.4.8` — Solana Program Metadata et complétude Token-2022 ## 1. Statut et rôle -Ce document est le livrable principal de `0.4.8-pre.001`, réconcilié après l’audit contractuel de `0.4.8-pre.002`, les modèles de `pre.003`, les décodeurs de comptes de `pre.004`, le décodeur d’instructions de `pre.005` et la matérialisation de `pre.006`. Il constitue le plan vivant de la version et reste modifiable lorsque l’audit du code, des interfaces officielles, de l’IDL ou des validations Devnet impose un ajustement. +Ce document est le livrable principal de `0.4.8-pre.001`, réconcilié après l’audit contractuel de `0.4.8-pre.002`, les modèles de `pre.003`, les décodeurs de comptes de `pre.004`, le décodeur d’instructions de `pre.005`, la matérialisation de `pre.006` et l’exécuteur de `pre.007`. Il constitue le plan vivant de la version et reste modifiable lorsque l’audit du code, des interfaces officielles, de l’IDL ou des validations Devnet impose un ajustement. Il doit être maintenu pendant chaque prerelease, puis archivé pendant la dernière prerelease après transfert des décisions durables vers le ROADMAP, les matrices, les guides, les rapports, les TODO et les changelogs concernés. @@ -15,13 +15,13 @@ Il doit être maintenu pendant chaque prerelease, puis archivé pendant la derni ### 2.1 Surfaces strictement distinctes -| Surface | Program ID ou frontière | Rôle dans `0.4.8` | -|---|---|---| -| Metaplex Token Metadata | `metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s` | surface clôturée en `0.4.7`, hors développement principal | -| Token-2022 Token Metadata | programme Token-2022 `TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb` | compléter le contrat déjà partiellement pris en charge | -| `spl-token-metadata-interface` | interface sans Program ID autonome imposé | source contractuelle des instructions metadata implémentées par Token-2022 | -| Solana Program Metadata | `ProgM6JCCvbYkfKqJYHePx4xxSUSqJp7rh8Lyv7nk7S` | nouvelle surface on-chain à implémenter sous `metadata/solana_program_metadata` | -| contenu distant HTTP/IPFS/Arweave | transport off-chain indépendant | hors `0.4.8`, reporté à l’horizon `0.15+` | +| Surface | Program ID ou frontière | Rôle dans `0.4.8` | +|-----------------------------------|--------------------------------------------------------------------|---------------------------------------------------------------------------------| +| Metaplex Token Metadata | `metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s` | surface clôturée en `0.4.7`, hors développement principal | +| Token-2022 Token Metadata | programme Token-2022 `TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb` | compléter le contrat déjà partiellement pris en charge | +| `spl-token-metadata-interface` | interface sans Program ID autonome imposé | source contractuelle des instructions metadata implémentées par Token-2022 | +| Solana Program Metadata | `ProgM6JCCvbYkfKqJYHePx4xxSUSqJp7rh8Lyv7nk7S` | nouvelle surface on-chain à implémenter sous `metadata/solana_program_metadata` | +| contenu distant HTTP/IPFS/Arweave | transport off-chain indépendant | hors `0.4.8`, reporté à l’horizon `0.15+` | Aucun décodeur Token-2022 existant n’est un décodeur partiel de `ProgM6…`. Les deux contrats doivent conserver des namespaces, modèles, matrices, tests et scénarios distincts. @@ -259,16 +259,22 @@ Résultat durable : les deux comptes décodés et les neuf instructions stables - matrice de onze projections et test de façade externe ; - aucune résolution de contenu distant ni lecture implicite d’un compte externe. -Critère atteint côté delta : tout ce qui est décodé dans la surface `ProgM6…` est matérialisable. Compilation, Clippy et tests Rust restent à exécuter dans l’environnement du projet avant validation définitive. +Validation définitive reçue le 5 août 2026 : `cargo fmt --all`, `cargo check --workspace`, `cargo clippy --all-targets` et l’audit workspace sont propres ; `cargo test -p kb-lib` réussit avec 667 tests unitaires, les tests d’API externes et les doc-tests. -### `0.4.8-pre.007` — builders et exécuteur Solana Program Metadata +### `0.4.8-pre.007` — builders et exécuteur Solana Program Metadata — terminé côté delta -- PDA, calculs de taille et rent ; -- builders contractuellement équivalents pour toutes les opérations courantes, y compris les mutations destructrices, irréversibles ou écrasantes ; -- politiques de signataires, autorités, coûts, confirmations, postconditions et capacités ; -- intégration explicite au cadre commun `crate::ExSafetyChecker` : le builder expose le plan complet, tandis que la politique de sécurité décide si ce plan peut progresser vers la simulation, la signature et l’envoi ; -- classification opérationnelle conforme aux règles : opérations courantes exécutables, opérations officiellement obsolètes annotées `#[deprecated]` avec approbation explicite, versions officiellement remplacées conservées en decode-only ; -- état audit actuel : les neuf instructions stables `0..8` sont courantes, sans instruction remplacée ou obsolète, donc toutes les neuf doivent être ciblées par l’exécuteur sauf découverte officielle contraire documentée avant code. +Résultat durable : les neuf instructions stables possèdent une opération typée, un builder wire exact et une capacité d’exécution. Les mutations dangereuses restent constructibles mais exigent une approbation explicite ; tous les plans traversent `ExSafetyChecker` avant d’être remis à l’orchestration. + +- helpers de PDA canonical et non-canonical avec seed exact de 16 octets ; +- sélection de canonicité explicite dans l’intent, indépendante de la seule présence de `ProgramData` ; +- builders des neuf opérations, comptes positionnels, signataires et variantes inline/Buffer ; +- simulation et quatre postconditions obligatoires ; +- approbation explicite pour `Write`, `SetAuthority`, `SetData`, `SetImmutable`, `Trim` et `Close` ; +- matrice d’exécution et test d’API externe ; +- aucune opération stable classée deprecated ou replaced faute de preuve officielle ; +- financement de rent volontairement laissé à la fixture et à l’orchestration RPC : le programme exige que les comptes à allouer ou étendre soient préfinancés. + +Travaux parallèles intégrés : les fonctions de matérialisation Metaplex existaient déjà dans `materializer/metadata/core.rs`; il ne s’agissait donc pas d’un oubli fonctionnel de `0.4.6/0.4.7`. Elles sont déplacées dans un sous-module dédié sans changement de clé métier. Token-2022 reçoit également son composant state-only dédié. L’audit est ensuite étendu à l’ensemble de `src/materializer` : 25 identités sont harmonisées, les façades deviennent pures, les implémentations utilisent `materializer.rs`, les constantes sont centralisées et les coquilles réservées ne produisent plus de faux événements legacy. Les constats sont consignés dans `docs/audits/V0_4_8_PRE_007_METAPLEX_MATERIALIZER_STRUCTURE_AUDIT.md` et `docs/audits/V0_4_8_PRE_007_MATERIALIZER_CONVENTION_AUDIT.md`. Un audit transversal de toutes les surfaces déjà livrées est planifié en `0.5.x` pour vérifier les règles de décodage exhaustif, propriété de matérialisation, exécution des opérations dangereuses via `ExSafetyChecker`, dépréciation officielle et versions remplacées decode-only. ### `0.4.8-pre.008` — pipeline stateful Solana Program Metadata @@ -283,6 +289,9 @@ Critère atteint côté delta : tout ce qui est décodé dans la surface `ProgM6 ### `0.4.8-pre.010` — complétude Token-2022 Token Metadata - implémentation des écarts confirmés de `Initialize`, `UpdateField`, `RemoveKey`, `UpdateAuthority` et `Emit` ; +- conservation du matérialiseur d’état dédié `metadata/token_2022/` pour les extensions `TokenMetadata`, `TokenGroup` et `TokenGroupMember` ; +- ajout d’une matérialisation explicite des cinq observations d’instructions `spl-token-metadata-interface`, actuellement absente ; +- maintien de `InitializeMetadataPointer` et `UpdateMetadataPointer` dans le matérialiseur d’administration qui en possède déjà les faits ; - tests synthétiques et stateful. ### `0.4.8-pre.011` — campagne Devnet Token-2022 Token Metadata diff --git a/docs/rules/RULES_SPECIFIC_KHADHROONY.md b/docs/rules/RULES_SPECIFIC_KHADHROONY.md index f7202a4..c54c701 100644 --- a/docs/rules/RULES_SPECIFIC_KHADHROONY.md +++ b/docs/rules/RULES_SPECIFIC_KHADHROONY.md @@ -1,5 +1,5 @@ - + # Règles spécifiques à `khadhroony-bot3` @@ -53,6 +53,13 @@ Toute divergence avec une règle générale doit être explicitement documentée - Une même réalité métier doit avoir une seule projection canonique. Les snapshots de comptes sont la source autoritative de l'état final lorsqu'ils sont disponibles ; les instructions et événements corrélés restent des observations de mutation et ne doivent pas produire un doublon concurrent du même état. - Les matérialisateurs doivent refuser les transactions non commitées pour les mutations d'état, tout en pouvant conserver séparément une intention non commitée lorsque le modèle métier le prévoit explicitement. - Toute absence volontaire de projection doit être documentée avec une justification technique précise. Un matérialisateur ne doit inventer ni état final, ni montant, ni autorité, ni frais, ni agrégation que l'observation ne prouve pas. +- Chaque matérialisateur concret possède un composant structuré avec une façade limitée aux déclarations de sous-modules et réexports, un `constants.rs` et un `materializer.rs`. Les fichiers spécialisés `state.rs`, `account.rs` ou équivalents sont ajoutés seulement lorsque leur responsabilité est distincte. +- L’identité runtime suit `kb-lib.materializer.[.]`. Le `processorName` persisté suit `materializer.[.]` et doit être exactement l’identité runtime sans le préfixe `kb-lib.`. Le tracing target d’un composant actif est identique à son identité runtime. +- Les constantes d’un composant utilisent le préfixe `MT__`, résident dans son `constants.rs`, sont réexportées par chaque façade jusqu’à `kb-lib/src/lib.rs` et sont consommées par `crate::`, y compris dans les tests lorsqu’elles sont `pub` ou `pub(crate)`. +- Toute sortie persistée contient un `processorName` issu de la constante du composant et une `projectionVersion` explicite. Une correction de provenance persistée impose une nouvelle version de projection. +- Une coquille réservée implémente `MtMaterializer` et `MtApiEventMaterializer`, expose une constante `MT_*_ACCEPTED_FAMILIES` vide, refuse tous les événements, retourne une liste legacy vide et un résultat contextuel `Ignored`. Elle ne doit jamais créer un faux événement de transition. +- Un composant state-only peut exposer uniquement des APIs de snapshots sans implémenter `MtApiEventMaterializer`; ce statut doit être explicite et ne crée pas artificiellement un matérialisateur instructionnel. +- Un wrapper public de compatibilité délègue vers le composant canonique et conserve exactement la même identité. Il ne constitue pas un second propriétaire de faits. ### Couverture des exécuteurs @@ -178,7 +185,7 @@ Aucun renommage massif de modules n'est autorisé sans étape de contrôle dédi - Les targets historiques `khbot.*` et les targets de fenêtre sont interdits dans les nouvelles modifications. - Les macros `tracing` doivent utiliser `target: crate::TRACING_TARGET` et des champs structurés stables. La granularité interne passe par `action`, `stage`, `window`, `campaign_id`, `signature`, `instruction_path`, `program_id`, `processor_name`, `processor_version`, `status` et `error_code`. - Les crates passives de types, contrats, DTO, API sans exécution, registres ou constantes restent sans dépendance `tracing`. `kb-config` reste une exception de bootstrap tant que sa validation précède l’installation du subscriber. -- Il est interdit d’ajouter `tracing` sans événement réel ou de conserver un faux target uniquement consommé par `let _target`. +- Il est interdit d’ajouter `tracing` sans événement réel, de conserver un tracing target déclaré mais inutilisé ou de consommer artificiellement un target avec `let _target`. - Une décision interne doit être journalisée par la crate responsable ; `kb-app-demo-desktop` ne journalise que les frontières Tauri/UI, les actions utilisateur et les résumés d’orchestration. - Tout input sélectionné sans décodeur compatible, tout résultat de décodage `failed` ou `unsupported`, tout résultat de matérialisation `failed`, toute validation de résultat invalide et toute erreur de persistance doivent émettre un événement `error` avant le retour ou la persistance terminale. - Une transaction Solana échouée mais correctement décodée n’est pas une erreur du logiciel. Une décision `ignored`, un refus de matérialisation conforme à la politique ou une annulation coopérative ne doivent pas être promus artificiellement au niveau `error`. @@ -187,7 +194,7 @@ Aucun renommage massif de modules n'est autorisé sans étape de contrôle dédi - Chaque profil doit router les événements vers les sorties globales `debug.log`, `info.log`, `error.jsonl` et `app.log`, puis vers `debug.log`, `info.log` et `error.jsonl` dans un répertoire propre à chaque crate utilisant `tracing`. - L’ajout ou la suppression de `tracing.workspace = true` dans une crate impose la mise à jour simultanée de la matrice de routes, de ses tests et de `docs/TRACING_CONTRACT.md`. - Le contrat détaillé est défini dans `docs/TRACING_CONTRACT.md`. -- L’audit mécanique spécifique au projet est exécuté par `python3 scripts/audit_khadhroony_workspace_rules.py`. Il couvre notamment `TRACING_TARGET_DECODER_METADATA_METAPLEX_TOKEN_METADATA` et l’usage obligatoire de `solana_pubkey::Pubkey` à la place de `solana_address::Address`. +- L’audit mécanique spécifique au projet est exécuté par `python3 scripts/audit_khadhroony_workspace_rules.py`. Il couvre notamment les conventions de composants et `processorName` des matérialisateurs, la pureté de leurs façades, la neutralité des coquilles réservées, `TRACING_TARGET_DECODER_METADATA_METAPLEX_TOKEN_METADATA` et l’usage obligatoire de `solana_pubkey::Pubkey` à la place de `solana_address::Address`. ## Règles de réutilisation des interfaces Solana et SPL diff --git a/kb-lib/CHANGELOG.md b/kb-lib/CHANGELOG.md index 826536a..b16fb71 100644 --- a/kb-lib/CHANGELOG.md +++ b/kb-lib/CHANGELOG.md @@ -1,8 +1,33 @@ - + # CHANGELOG — kb-lib +## 0.4.8-pre.007 — exécuteur Solana Program Metadata et structure Metaplex + +- ajoute `ExMetadataSolanaProgramMetadataExecutor`, les intents typés, les helpers de PDA et les builders wire exacts des neuf instructions stables `0..8` ; +- conserve une sélection explicite canonical/non-canonical au lieu de l’inférer de la présence de `ProgramData`, pour couvrir aussi les contrats des loaders antérieurs ; +- conserve toutes les opérations courantes comme exécutables, y compris `Write`, `SetAuthority`, `SetData`, `SetImmutable`, `Trim` et `Close`, avec approbation destructive explicite plutôt qu’une suppression de capacité ; +- soumet chaque plan préparé au cadre commun `ExSafetyChecker` et impose simulation, signataires autorisés, plafonds et quatre postconditions canoniques ; +- ajoute la matrice d’exécution, le test de parité des capacités et le test d’API externe depuis la façade de crate ; +- retire l’exécuteur Solana Program Metadata de l’inventaire réservé ; +- corrige le total attendu du test d’inventaire des exécuteurs réservés de `104` à `103`, conformément au retrait de `ExMetadataSolanaProgramMetadataExecutor` de cet inventaire ; +- confirme que les neuf opérations stables sont courantes et qu’aucune annotation `#[deprecated]` n’est justifiée par les sources officielles actuelles ; +- sépare les matérialisations Metaplex Token Metadata dans un sous-module dédié sans modifier les clés ni la sémantique métier des projections déjà livrées, et consigne le constat dans un audit structurel dédié ; +- corrige la confusion entre identité runtime `kb-lib.*` et `processorName` persisté : Metaplex utilise désormais `materializer.metadata.metaplex_token_metadata`, Solana Program Metadata `materializer.metadata.solana_program_metadata` et Token-2022 `materializer.metadata.token_2022` ; +- porte les schémas de projection Metaplex et Token-2022 à la version `2` pour refléter cette correction de provenance ; +- extrait la matérialisation Token-2022 de `metadata/core.rs` vers le composant dédié `metadata/token_2022/` sans modifier la fonction publique ni les clés de sortie ; +- confirme que les états `TokenMetadata`, `TokenGroup` et `TokenGroupMember` sont matérialisés, que les metadata pointers appartiennent déjà au matérialiseur d’administration et que les cinq faits d’instructions `spl-token-metadata-interface` restent à fermer en `pre.010` ; +- inscrit dans le TODO et le ROADMAP `0.5.x` un audit transversal des règles de complétude decoder/materializer/executor, de la sécurité et des statuts deprecated/replaced. +- harmonise les 25 composants de matérialisation autour des paires `MT_*_COMPONENT_NAME` / `MT_*_PROCESSOR_NAME`, avec relation exacte entre identité runtime `kb-lib.materializer.*` et processeur persisté `materializer.*` ; +- ajoute la provenance persistée manquante aux sorties `admin`, `compliance.audit`, `fees`, `lifecycle` et `staking`, puis porte leurs contrats à `projectionVersion = 2` ; +- remplace les implémentations génériques `core.rs` par des `materializer.rs`, transforme les fichiers parents en façades pures et déplace toutes les familles, surfaces, tables et identités vers les `constants.rs` réexportés jusqu’à la racine de crate ; +- rend les 14 matérialisateurs réservés réellement neutres sur les deux traits : famille vide constante, refus legacy, liste d’événements vide et résultat contextuel `Ignored` ; +- aligne les routes de logging d’exemple sur `compliance.audit`, `transaction.annotations` et `token.accounts`, et ajoute des audits mécaniques empêchant la réintroduction de noms, constantes ou façades non conformes ; +- conserve `MtMetadataMaterializer` comme wrapper de compatibilité du composant canonique `MtSolanaProgramMetadataMaterializer`, sans second `processorName` ni second propriétaire de faits. +- corrige les warnings de contrats opérationnels en ajoutant des événements `tracing` réels aux matérialisations `fees`, Metaplex Token Metadata et metadata Token-2022, avec `component_name`, `processor_name`, statut, identité de compte ou d’instruction et nombre de sorties ; +- consomme désormais l’identité runtime `MT_METADATA_TOKEN_2022_COMPONENT_NAME` dans l’implémentation state-only au lieu de la limiter aux réexports, et étend les audits pour refuser tout tracing target opérationnel déclaré mais inutilisé ainsi que toute identité de composant absente de son implémentation. + ## 0.4.8-pre.006 — matérialisation Solana Program Metadata - active `MtMetadataMaterializer` comme matérialiseur contextualisé de la surface exacte `metadata.solana_program_metadata` ; @@ -15,6 +40,7 @@ - ajoute les helpers publics de matérialisation d’états et le test d’API externe correspondant. - corrige le helper de test de snapshot afin d’utiliser les variantes wire existantes `Encoding::Utf8` et `Format::Json`. - précise que les opérations destructrices, irréversibles ou écrasantes restent exécutables : leur progression vers simulation, signature et envoi doit être gouvernée par le cadre commun `ExSafetyChecker`, et non supprimée du périmètre de l’exécuteur. +- validation reçue le 5 août 2026 : `cargo fmt`, `cargo check --workspace`, `cargo clippy --all-targets`, audit workspace et 667 tests unitaires `kb-lib` réussis, avec les tests d’API externes et les doc-tests. ## 0.4.8-pre.005 — instructions Solana Program Metadata diff --git a/kb-lib/README.md b/kb-lib/README.md index bd2d43a..4277596 100644 --- a/kb-lib/README.md +++ b/kb-lib/README.md @@ -1,5 +1,5 @@ - + # kb-lib @@ -20,10 +20,12 @@ Pour Solana Program Metadata, la surface publique fondatrice comprend : - une politique explicite où les octets situés après `data_length` sont conservés comme capacité allouée et jamais assimilés au contenu logique ; - les neuf instructions stables `Write`, `Initialize`, `SetAuthority`, `SetData`, `SetImmutable`, `Trim`, `Close`, `Allocate` et `Extend` ; - les formes runtime actuelles non produites par le SDK, notamment `SetData` sans remplacement de données et les suffixes ignorés de `SetImmutable`, `Trim` et `Close` ; -- une frontière historique où l’ancien nom pré-stable `WithdrawExcessLamports` du tag `5` reste une provenance de `Trim`, sans dixième entrée de couverture. +- une frontière historique où l’ancien nom pré-stable `WithdrawExcessLamports` du tag `5` reste une provenance de `Trim`, sans dixième entrée de couverture ; +- une sélection canonical/non-canonical explicite dans les intents `Initialize` et `Allocate`, indépendante de la seule présence de `ProgramData` ; - deux projections d’état autoritatives pour `Buffer` et `Metadata`, avec conservation bornée du contenu on-chain ; - neuf faits de mutation d’instructions committées, non autoritatifs vis-à-vis de l’état final et réconciliables avec les snapshots post-exécution ; -- une classification où les neuf instructions stables sont courantes, sans opération officiellement remplacée ou obsolète à annoter `Deprecated`. +- une classification où les neuf instructions stables sont courantes, sans opération officiellement remplacée ou obsolète à annoter `Deprecated` ; +- un exécuteur typé couvrant les neuf opérations, avec dérivation des PDA, wire exact, comptes ordonnés, signataires, approbation explicite des mutations dangereuses et évaluation commune par `ExSafetyChecker`. Pour Metaplex Token Metadata, elle fournit : @@ -32,7 +34,21 @@ Pour Metaplex Token Metadata, elle fournit : - 20 opérations courantes exécutables ; - 15 opérations obsolètes encore constructibles, exposées comme dépréciées et soumises à une approbation explicite ; - 22 versions remplacées conservées en décodage uniquement et redirigées vers leur remplacement canonique final ; -- une frontière explicite avec Bubblegum et avec les metadata incorporées de Token-2022. +- une frontière explicite avec Bubblegum et avec les metadata incorporées de Token-2022 ; +- un sous-module de matérialisation dédié `materializer/metadata/metaplex_token_metadata`, qui conserve les projections d’état déjà livrées sans modifier leurs clés ni leur sémantique persistée. + +## Architecture des matérialisateurs + +L’arbre `src/materializer` suit un contrat homogène : + +- une façade limitée aux déclarations de sous-modules et réexports ; +- un `constants.rs` propriétaire des identités, familles, surfaces, versions de projection et tracing targets ; +- un `materializer.rs` pour les implémentations de traits ; +- des fichiers `state.rs` ou `account.rs` uniquement lorsque la projection stateful l’exige ; +- une identité runtime `kb-lib.materializer.*` distincte du `processorName` persisté `materializer.*` ; +- des coquilles réservées réellement inactives sur les deux contrats legacy et contextuel. + +L’inventaire audité comprend 25 composants nommés : neuf matérialisateurs actifs, quatorze coquilles réservées et deux composants metadata state-only. Le détail est consigné dans [l’audit de convention des matérialisateurs](../docs/audits/V0_4_8_PRE_007_MATERIALIZER_CONVENTION_AUDIT.md). ## Responsabilités @@ -57,6 +73,7 @@ Pour Metaplex Token Metadata, elle fournit : - contrats `ExApi*`, politiques de sécurité et exécuteurs `Ex*Executor` ; - `ExMetadataMetaplexTokenMetadataExecutor`, `ExMetaplexTokenMetadataExecutionIntent` et `ExMetaplexTokenMetadataOperation` ; - `DcMetadataSolanaProgramMetadataDecoder`, modèles `DcMetadataSpm*`, constantes `DC_METADATA_SPM_*`, fonctions `decoder_metadata_solana_program_metadata_decode_*_account` et helpers `materializer_metadata_materialize_solana_program_metadata_*_snapshot` ; +- `ExMetadataSolanaProgramMetadataExecutor`, `ExMetadataSpmExecutionIntent`, `ExMetadataSpmOperation`, les helpers de PDA et les constantes `EX_METADATA_SPM_*_OPERATION` ; - modèles canoniques `Md*` réexportés par la façade. Les exemples d’appel et invariants sont documentés dans [USAGE.md](USAGE.md). @@ -71,7 +88,7 @@ Les exemples d’appel et invariants sont documentés dans [USAGE.md](USAGE.md). La surface fonctionnelle Metaplex de la crate est achevée. Les campagnes Devnet restantes relèvent de `kb-pipeline-demo-scenarios` et de `kb-app-demo-desktop`, non d’un manque de builder dans `kb-lib`. -Pour Solana Program Metadata, les modèles wire, les décodeurs bornés de comptes, le décodeur contextualisé et la matérialisation des deux comptes et des neuf instructions stables sont actifs. `Buffer` accepte tout reliquat comme données allouées sans inventer de PDA stable après changement d’autorité ; `Metadata` valide la dérivation canonical ou non-canonical et distingue le contenu logique de la capacité ajoutée par `Extend`. Les instructions produisent des faits committés non autoritatifs ; les snapshots post-exécution restent la source de l’état final. Les variantes qui dépendent de l’état du compte restent des intentions wire jusqu’aux lectures stateful prévues, et aucune forme pré-stable n’est déclarée compatible réseau sans preuve de cluster. +Pour Solana Program Metadata, les modèles wire, les décodeurs bornés de comptes, le décodeur contextualisé, la matérialisation des deux comptes et des neuf instructions stables, ainsi que l’exécuteur typé des neuf opérations sont actifs. `Buffer` accepte tout reliquat comme données allouées sans inventer de PDA stable après changement d’autorité ; `Metadata` valide la dérivation canonical ou non-canonical et distingue le contenu logique de la capacité ajoutée par `Extend`. Les instructions produisent des faits committés non autoritatifs ; les snapshots post-exécution restent la source de l’état final. Les variantes qui dépendent de l’état du compte restent des intentions wire jusqu’aux lectures stateful prévues, et aucune forme pré-stable n’est déclarée compatible réseau sans preuve de cluster. Le registre ElGamal n’est pas déclaré validé réellement sur réseau. @@ -84,4 +101,5 @@ Le registre ElGamal n’est pas déclaré validé réellement sur réseau. - [Matrice des comptes Solana Program Metadata](../test-fixtures/contract-matrices/SOLANA_PROGRAM_METADATA_ACCOUNT_MATRIX.json) - [Matrice des instructions Solana Program Metadata](../test-fixtures/contract-matrices/SOLANA_PROGRAM_METADATA_INSTRUCTION_MATRIX.json) - [Matrice de matérialisation Solana Program Metadata](../test-fixtures/contract-matrices/SOLANA_PROGRAM_METADATA_MATERIALIZATION_MATRIX.json) +- [Matrice d’exécution Solana Program Metadata](../test-fixtures/contract-matrices/SOLANA_PROGRAM_METADATA_EXECUTOR_MATRIX.json) - [Audit de l’inventaire et de l’historique des instructions](../docs/audits/V0_4_8_PRE_005_SOLANA_PROGRAM_METADATA_INSTRUCTION_HISTORY_AUDIT.md) diff --git a/kb-lib/TODO.md b/kb-lib/TODO.md index e62f307..831b9ba 100644 --- a/kb-lib/TODO.md +++ b/kb-lib/TODO.md @@ -1,15 +1,17 @@ - + # TODO — kb-lib -## Réaudit ultérieur des exécuteurs déjà livrés +## `0.5.x` — audit transversal décodage, matérialisation, exécution et sécurité -- [ ] Version à déterminer - réauditer les exécuteurs Solana Core et SPL selon la règle distinguant les opérations courantes, remplacées et obsolètes encore constructibles. -- [ ] Solana Core - vérifier chaque génération historique ou obsolète officiellement constructible et ajouter les opérations dépréciées manquantes sans réexposer les versions remplacées intermédiaires. -- [ ] SPL Memo - réexaminer Memo v1 et Memo v3 et documenter leur statut exact de remplacement ou d’obsolescence constructible. -- [ ] SPL Token, ATA, Token-2022 et registre ElGamal - appliquer la même classification opération par opération. -- [ ] Matrices - aligner statuts, annotations `#[deprecated]`, approbations opérateur et tests de parité des surfaces réauditées. +- [ ] Inventaire - recenser chaque surface courante, historique, remplacée et officiellement obsolète de tous les décodeurs déjà livrés. +- [ ] Décodage - vérifier que chaque décodeur couvre exhaustivement son contrat officiel, y compris les formes historiques qui restent observables. +- [ ] Matérialisation - vérifier que chaque état ou fait décodé possède exactement un propriétaire de matérialisation, sans omission ni double projection. +- [ ] Exécution - vérifier que chaque opération courante, destructive, irréversible ou écrasante officiellement constructible possède un builder et une capacité d’exécution. +- [ ] Sécurité - vérifier que chaque plan exécutable traverse `ExSafetyChecker`, déclare ses signataires, plafonds, confirmations et postconditions, sans supprimer une opération au seul motif qu’elle est dangereuse. +- [ ] Dépréciation - annoter `#[deprecated]` les opérations officiellement obsolètes mais encore constructibles, conserver leur statut exécutable avec approbation explicite et laisser les versions officiellement remplacées en decode-only. +- [ ] Matrices et documentation - réconcilier statuts, fact ownership, capacités, approbations opérateur, tests de parité, README, USAGE et changelogs pour Solana Core, SPL, Metaplex et les surfaces ajoutées avant `0.5.x`. ## Report conditionnel — registre SPL ElGamal diff --git a/kb-lib/USAGE.md b/kb-lib/USAGE.md index fa4844b..7f9f6f1 100644 --- a/kb-lib/USAGE.md +++ b/kb-lib/USAGE.md @@ -1,5 +1,5 @@ - + # Utilisation de kb-lib @@ -226,7 +226,8 @@ Les signatures exactes doivent être vérifiées lors de l’utilisation : certa - `ExSplTokenExecutor` ; - `ExSplAssociatedTokenAccountExecutor` ; - `ExSplToken2022Executor` ; -- `ExSplElgamalRegistryExecutor`. +- `ExSplElgamalRegistryExecutor` ; +- `ExMetadataSolanaProgramMetadataExecutor`. ```rust use kb_lib::{ExApiTypedInstructionExecutor, ExSplMemoExecutor}; @@ -235,7 +236,6 @@ let executor = ExSplMemoExecutor; let plan = ExApiTypedInstructionExecutor::build_prepared_plan( &executor, &intent, - &policy, ); ``` @@ -256,6 +256,41 @@ println!("required_signers={}", plan.required_signers.len()); Memo v1 et v3 restent non exécutables. Memo v4 est la seule génération exécutable. + +## Exécution Solana Program Metadata + +`ExMetadataSolanaProgramMetadataExecutor` couvre les neuf opérations stables `Write`, `Initialize`, `SetAuthority`, `SetData`, `SetImmutable`, `Trim`, `Close`, `Allocate` et `Extend`. Les helpers publics dérivent les PDA canonical et non-canonical ; l’appelant doit toujours fournir et vérifier le compte attendu avant construction du plan. + +Les mutations écrasantes, destructrices ou irréversibles ne sont pas retirées de l’API. Elles exigent `allow_destructive_operation = true`, puis le plan complet est évalué par `ExSafetyChecker`. Cette approbation locale ne remplace ni la simulation RPC, ni la confirmation opérateur, ni les postconditions stateful. + +```rust +let intent = kb_lib::ExMetadataSpmExecutionIntent { + intent_id: "extend-program-metadata".to_string(), + fee_payer: authority.clone(), + policy, + allow_destructive_operation: false, + operation: kb_lib::ExMetadataSpmOperation::Extend { + account: metadata_account, + authority, + length: 256, + program_context: std::option::Option::None, + }, +}; +let executor = kb_lib::ExMetadataSolanaProgramMetadataExecutor; +let plan = match kb_lib::ExApiTypedInstructionExecutor::build_prepared_plan( + &executor, + &intent, +) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), +}; +``` + +`Initialize` et `Allocate` n’inventent pas le financement préalable nécessaire à l’exemption de rent. La préparation de fixture ou l’orchestration stateful doit calculer le financement depuis le RPC, préfinancer le PDA ou le compte keypair, puis simuler le message exact avant soumission. + +Les contenus inline sont volontairement bornés à 1 024 octets par le workspace. Les contenus plus grands passent par un compte `Buffer` préfinancé, écrit par segments puis consommé par `Initialize` ou `SetData`. +Pour `Initialize` et les allocations de Buffer PDA, le booléen `canonical` est fourni explicitement. La présence de `program_data` ne suffit pas à déterminer la dérivation, car les programmes issus de loaders antérieurs peuvent être canoniques sans compte ProgramData. + ## Exécution Metaplex Token Metadata La surface publique repose sur : diff --git a/kb-lib/src/decoder/metadata/metaplex_token_metadata/decoder.rs b/kb-lib/src/decoder/metadata/metaplex_token_metadata/decoder.rs index 88850af..1891996 100644 --- a/kb-lib/src/decoder/metadata/metaplex_token_metadata/decoder.rs +++ b/kb-lib/src/decoder/metadata/metaplex_token_metadata/decoder.rs @@ -1,5 +1,5 @@ // file: kb-lib/src/decoder/metadata/metaplex_token_metadata/decoder.rs -// version: 31 +// version: 32 //! Exact contextual dispatch for Metaplex Token Metadata. @@ -144,9 +144,6 @@ impl crate::DcApiInstructionDecoder for crate::DcMetadataMetaplexTokenMetadataDe mod tests { use base64::Engine; // rust-rules: trait-import - const AUDIT_MATRIX_JSON: &str = include_str!( - "../../../../../test-fixtures/contract-matrices/METAPLEX_TOKEN_METADATA_MATRIX.json" - ); const MAX_SDK_INSTRUCTION_MODULES: usize = 128; const MIN_SDK_INSTRUCTION_MODULES: usize = 90; const MIN_PDA_FAMILIES: usize = 8; @@ -2023,7 +2020,9 @@ mod tests { #[test] fn audit_matrix_remains_machine_readable_and_bounded() { - let parsed = serde_json::from_str::(AUDIT_MATRIX_JSON); + let parsed = serde_json::from_str::(include_str!( + "../../../../../test-fixtures/contract-matrices/METAPLEX_TOKEN_METADATA_MATRIX.json" + )); assert!(parsed.is_ok()); let matrix = match parsed { std::result::Result::Ok(value) => value, @@ -3053,7 +3052,9 @@ mod tests { #[test] fn pre_002_contract_closes_all_instruction_statuses_and_idl_accounts() { - let matrix: serde_json::Value = match serde_json::from_str(AUDIT_MATRIX_JSON) { + let matrix: serde_json::Value = match serde_json::from_str(include_str!( + "../../../../../test-fixtures/contract-matrices/METAPLEX_TOKEN_METADATA_MATRIX.json" + )) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("matrix parse failed: {error}"), }; @@ -3093,7 +3094,9 @@ mod tests { #[test] fn pre_002_fact_ownership_is_unique_and_keeps_metadata_domains_separate() { - let matrix: serde_json::Value = match serde_json::from_str(AUDIT_MATRIX_JSON) { + let matrix: serde_json::Value = match serde_json::from_str(include_str!( + "../../../../../test-fixtures/contract-matrices/METAPLEX_TOKEN_METADATA_MATRIX.json" + )) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("matrix parse failed: {error}"), }; diff --git a/kb-lib/src/decoder/metadata/solana_program_metadata/account.rs b/kb-lib/src/decoder/metadata/solana_program_metadata/account.rs index 3c97c3a..6116a73 100644 --- a/kb-lib/src/decoder/metadata/solana_program_metadata/account.rs +++ b/kb-lib/src/decoder/metadata/solana_program_metadata/account.rs @@ -1,5 +1,5 @@ // file: kb-lib/src/decoder/metadata/solana_program_metadata/account.rs -// version: 2 +// version: 3 //! Bounded account decoding for the Solana Program Metadata program. @@ -61,66 +61,61 @@ pub enum DcMetadataSpmAccountDecodeError { impl std::fmt::Display for DcMetadataSpmAccountDecodeError { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { + return match self { Self::InvalidAccountKey => { - return formatter.write_str("program metadata account key is invalid"); - }, - Self::InvalidOwner => { - return formatter.write_str("program metadata account owner is invalid"); + formatter.write_str("program metadata account key is invalid") }, + Self::InvalidOwner => formatter.write_str("program metadata account owner is invalid"), Self::ForeignOwner => { - return formatter.write_str("program metadata account is owned by another program"); - }, - Self::EmptyAccountData => { - return formatter.write_str("program metadata account data is empty"); + formatter.write_str("program metadata account is owned by another program") }, + Self::EmptyAccountData => formatter.write_str("program metadata account data is empty"), Self::UninitializedAccount => { - return formatter.write_str("program metadata account is uninitialized"); + formatter.write_str("program metadata account is uninitialized") }, Self::AccountDataTooLarge { actual_bytes, max_bytes } => { - return write!( + write!( formatter, "program metadata account data length {actual_bytes} exceeds {max_bytes} bytes", - ); + ) }, Self::TruncatedHeader { actual_bytes, minimum_bytes } => { - return write!( + write!( formatter, "program metadata account header length {actual_bytes} is below {minimum_bytes} bytes", - ); + ) }, Self::UnexpectedAccountDiscriminator { expected, actual } => { - return write!( + write!( formatter, "program metadata account discriminator {actual:?} does not match {expected:?}", - ); + ) }, Self::LogicalDataOutOfBounds { declared_bytes, available_bytes } => { - return write!( + write!( formatter, "program metadata logical data length {declared_bytes} exceeds {available_bytes} allocated bytes", - ); + ) }, Self::LogicalDataLengthUnsupported { declared_bytes } => { - return write!( + write!( formatter, "program metadata logical data length {declared_bytes} cannot be represented on this architecture", - ); + ) }, Self::MissingNonCanonicalAuthority => { - return formatter - .write_str("non-canonical program metadata account has no authority"); + formatter.write_str("non-canonical program metadata account has no authority") }, Self::InvalidMetadataPda => { - return formatter.write_str("program metadata account PDA is invalid"); + formatter.write_str("program metadata account PDA is invalid") }, Self::InvalidUrlUtf8 => { - return formatter.write_str("program metadata URL data is not valid UTF-8"); + formatter.write_str("program metadata URL data is not valid UTF-8") }, Self::Model(error) => { - return write!(formatter, "program metadata model conversion failed: {error}"); + write!(formatter, "program metadata model conversion failed: {error}") }, - } + }; } } @@ -207,7 +202,7 @@ pub fn decoder_metadata_solana_program_metadata_decode_account( return std::result::Result::Err(crate::DcMetadataSpmAccountDecodeError::Model(error)); }, }; - match discriminator { + return match discriminator { crate::DcMetadataSpmAccountDiscriminator::Buffer => { let snapshot = match crate::decoder_metadata_solana_program_metadata_decode_buffer_account( @@ -216,7 +211,7 @@ pub fn decoder_metadata_solana_program_metadata_decode_account( std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; - return std::result::Result::Ok(crate::DcMetadataSpmAccountSnapshot::Buffer(snapshot)); + std::result::Result::Ok(crate::DcMetadataSpmAccountSnapshot::Buffer(snapshot)) }, crate::DcMetadataSpmAccountDiscriminator::Metadata => { let snapshot = @@ -226,16 +221,12 @@ pub fn decoder_metadata_solana_program_metadata_decode_account( std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; - return std::result::Result::Ok(crate::DcMetadataSpmAccountSnapshot::Metadata( - snapshot, - )); + std::result::Result::Ok(crate::DcMetadataSpmAccountSnapshot::Metadata(snapshot)) }, crate::DcMetadataSpmAccountDiscriminator::Empty => { - return std::result::Result::Err( - crate::DcMetadataSpmAccountDecodeError::UninitializedAccount, - ); + std::result::Result::Err(crate::DcMetadataSpmAccountDecodeError::UninitializedAccount) }, - } + }; } /// Decodes one Solana Program Metadata Buffer account without claiming a stable PDA after authority changes. @@ -530,12 +521,12 @@ fn decoder_metadata_solana_program_metadata_decode_typed_data( crate::DcMetadataSpmData::External(external) }, }; - match model.validate() { - std::result::Result::Ok(()) => return std::result::Result::Ok(model), + return match model.validate() { + std::result::Result::Ok(()) => std::result::Result::Ok(model), std::result::Result::Err(error) => { - return std::result::Result::Err(crate::DcMetadataSpmAccountDecodeError::Model(error)); + std::result::Result::Err(crate::DcMetadataSpmAccountDecodeError::Model(error)) }, - } + }; } fn decoder_metadata_solana_program_metadata_validate_metadata_pda( @@ -576,12 +567,12 @@ fn decoder_metadata_solana_program_metadata_validate_metadata_pda( #[cfg(test)] mod tests { fn metadata_program_id() -> std::option::Option { - match ::from_str( + return match ::from_str( kb_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID, ) { - std::result::Result::Ok(value) => return std::option::Option::Some(value), - std::result::Result::Err(_) => return std::option::Option::None, - } + std::result::Result::Ok(value) => std::option::Option::Some(value), + std::result::Result::Err(_) => std::option::Option::None, + }; } #[derive(Clone, Copy)] diff --git a/kb-lib/src/decoder/metadata/solana_program_metadata/instruction.rs b/kb-lib/src/decoder/metadata/solana_program_metadata/instruction.rs index ce251eb..6fcd6bb 100644 --- a/kb-lib/src/decoder/metadata/solana_program_metadata/instruction.rs +++ b/kb-lib/src/decoder/metadata/solana_program_metadata/instruction.rs @@ -1,5 +1,5 @@ // file: kb-lib/src/decoder/metadata/solana_program_metadata/instruction.rs -// version: 3 +// version: 4 //! Exact contextual instruction decoding for Solana Program Metadata. @@ -890,10 +890,6 @@ fn failed( mod tests { use base64::Engine; // rust-rules: trait-import - const MATRIX_JSON: &str = include_str!( - "../../../../../test-fixtures/contract-matrices/SOLANA_PROGRAM_METADATA_INSTRUCTION_MATRIX.json" - ); - fn input( payload: &[u8], accounts: &[(&str, bool, bool)], @@ -1262,7 +1258,9 @@ mod tests { #[test] fn instruction_matrix_is_machine_readable_and_matches_compiled_inventory() { - let matrix: serde_json::Value = match serde_json::from_str(MATRIX_JSON) { + let matrix: serde_json::Value = match serde_json::from_str(include_str!( + "../../../../../test-fixtures/contract-matrices/SOLANA_PROGRAM_METADATA_INSTRUCTION_MATRIX.json" + )) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("instruction matrix must parse: {error}"), }; diff --git a/kb-lib/src/decoder/metadata/solana_program_metadata/model.rs b/kb-lib/src/decoder/metadata/solana_program_metadata/model.rs index 8b2eb71..da9af02 100644 --- a/kb-lib/src/decoder/metadata/solana_program_metadata/model.rs +++ b/kb-lib/src/decoder/metadata/solana_program_metadata/model.rs @@ -1,5 +1,5 @@ // file: kb-lib/src/decoder/metadata/solana_program_metadata/model.rs -// version: 2 +// version: 3 //! Public bounded models for the Solana Program Metadata wire contract. @@ -24,16 +24,14 @@ impl DcMetadataSpmAccountDiscriminator { /// Resolves an exact wire discriminant. pub fn from_wire_value(value: u8) -> std::result::Result { - match value { - 0 => return std::result::Result::Ok(Self::Empty), - 1 => return std::result::Result::Ok(Self::Buffer), - 2 => return std::result::Result::Ok(Self::Metadata), - _ => { - return std::result::Result::Err( - crate::DcMetadataSpmModelError::UnknownAccountDiscriminator { value }, - ); - }, - } + return match value { + 0 => std::result::Result::Ok(Self::Empty), + 1 => std::result::Result::Ok(Self::Buffer), + 2 => std::result::Result::Ok(Self::Metadata), + _ => std::result::Result::Err( + crate::DcMetadataSpmModelError::UnknownAccountDiscriminator { value }, + ), + }; } } @@ -60,17 +58,15 @@ impl DcMetadataSpmEncoding { /// Resolves an exact wire discriminant. pub fn from_wire_value(value: u8) -> std::result::Result { - match value { - 0 => return std::result::Result::Ok(Self::None), - 1 => return std::result::Result::Ok(Self::Utf8), - 2 => return std::result::Result::Ok(Self::Base58), - 3 => return std::result::Result::Ok(Self::Base64), + return match value { + 0 => std::result::Result::Ok(Self::None), + 1 => std::result::Result::Ok(Self::Utf8), + 2 => std::result::Result::Ok(Self::Base58), + 3 => std::result::Result::Ok(Self::Base64), _ => { - return std::result::Result::Err(crate::DcMetadataSpmModelError::UnknownEncoding { - value, - }); + std::result::Result::Err(crate::DcMetadataSpmModelError::UnknownEncoding { value }) }, - } + }; } } @@ -95,16 +91,14 @@ impl DcMetadataSpmCompression { /// Resolves an exact wire discriminant. pub fn from_wire_value(value: u8) -> std::result::Result { - match value { - 0 => return std::result::Result::Ok(Self::None), - 1 => return std::result::Result::Ok(Self::Gzip), - 2 => return std::result::Result::Ok(Self::Zlib), - _ => { - return std::result::Result::Err( - crate::DcMetadataSpmModelError::UnknownCompression { value }, - ); - }, - } + return match value { + 0 => std::result::Result::Ok(Self::None), + 1 => std::result::Result::Ok(Self::Gzip), + 2 => std::result::Result::Ok(Self::Zlib), + _ => std::result::Result::Err(crate::DcMetadataSpmModelError::UnknownCompression { + value, + }), + }; } } @@ -131,17 +125,13 @@ impl DcMetadataSpmFormat { /// Resolves an exact wire discriminant. pub fn from_wire_value(value: u8) -> std::result::Result { - match value { - 0 => return std::result::Result::Ok(Self::None), - 1 => return std::result::Result::Ok(Self::Json), - 2 => return std::result::Result::Ok(Self::Yaml), - 3 => return std::result::Result::Ok(Self::Toml), - _ => { - return std::result::Result::Err(crate::DcMetadataSpmModelError::UnknownFormat { - value, - }); - }, - } + return match value { + 0 => std::result::Result::Ok(Self::None), + 1 => std::result::Result::Ok(Self::Json), + 2 => std::result::Result::Ok(Self::Yaml), + 3 => std::result::Result::Ok(Self::Toml), + _ => std::result::Result::Err(crate::DcMetadataSpmModelError::UnknownFormat { value }), + }; } } @@ -166,16 +156,14 @@ impl DcMetadataSpmDataSource { /// Resolves an exact wire discriminant. pub fn from_wire_value(value: u8) -> std::result::Result { - match value { - 0 => return std::result::Result::Ok(Self::Direct), - 1 => return std::result::Result::Ok(Self::Url), - 2 => return std::result::Result::Ok(Self::External), - _ => { - return std::result::Result::Err( - crate::DcMetadataSpmModelError::UnknownDataSource { value }, - ); - }, - } + return match value { + 0 => std::result::Result::Ok(Self::Direct), + 1 => std::result::Result::Ok(Self::Url), + 2 => std::result::Result::Ok(Self::External), + _ => std::result::Result::Err(crate::DcMetadataSpmModelError::UnknownDataSource { + value, + }), + }; } } @@ -204,18 +192,16 @@ impl DcMetadataSpmProgramError { /// Resolves an exact custom program error code. pub fn from_code(value: u32) -> std::result::Result { - match value { - 0 => return std::result::Result::Ok(Self::NotExecutableAccount), - 1 => return std::result::Result::Ok(Self::InvalidProgramState), - 2 => return std::result::Result::Ok(Self::InvalidProgramDataAccount), - 3 => return std::result::Result::Ok(Self::ImmutableMetadataAccount), - 4 => return std::result::Result::Ok(Self::InvalidDataLength), - _ => { - return std::result::Result::Err( - crate::DcMetadataSpmModelError::UnknownProgramError { value }, - ); - }, - } + return match value { + 0 => std::result::Result::Ok(Self::NotExecutableAccount), + 1 => std::result::Result::Ok(Self::InvalidProgramState), + 2 => std::result::Result::Ok(Self::InvalidProgramDataAccount), + 3 => std::result::Result::Ok(Self::ImmutableMetadataAccount), + 4 => std::result::Result::Ok(Self::InvalidDataLength), + _ => std::result::Result::Err(crate::DcMetadataSpmModelError::UnknownProgramError { + value, + }), + }; } } @@ -272,12 +258,12 @@ impl DcMetadataSpmSeed { while length > 0 && self.0[length - 1] == 0 { length -= 1; } - match std::str::from_utf8(&self.0[..length]) { - std::result::Result::Ok(value) => return std::result::Result::Ok(value), + return match std::str::from_utf8(&self.0[..length]) { + std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Err(_) => { - return std::result::Result::Err(crate::DcMetadataSpmModelError::InvalidSeedUtf8); + std::result::Result::Err(crate::DcMetadataSpmModelError::InvalidSeedUtf8) }, - } + }; } } @@ -361,11 +347,11 @@ pub enum DcMetadataSpmData { impl DcMetadataSpmData { /// Returns the source discriminator associated with this content. pub const fn source(&self) -> crate::DcMetadataSpmDataSource { - match self { - Self::Direct(_) => return crate::DcMetadataSpmDataSource::Direct, - Self::Url(_) => return crate::DcMetadataSpmDataSource::Url, - Self::External(_) => return crate::DcMetadataSpmDataSource::External, - } + return match self { + Self::Direct(_) => crate::DcMetadataSpmDataSource::Direct, + Self::Url(_) => crate::DcMetadataSpmDataSource::Url, + Self::External(_) => crate::DcMetadataSpmDataSource::External, + }; } /// Validates source-specific length requirements and the `u32` header bound. @@ -389,11 +375,11 @@ impl DcMetadataSpmData { /// Returns the exact number of bytes represented on wire. pub fn wire_byte_len(&self) -> usize { - match self { - Self::Direct(value) => return value.len(), - Self::Url(value) => return value.len(), - Self::External(_) => return crate::DC_METADATA_SPM_EXTERNAL_DATA_BYTES, - } + return match self { + Self::Direct(value) => value.len(), + Self::Url(value) => value.len(), + Self::External(_) => crate::DC_METADATA_SPM_EXTERNAL_DATA_BYTES, + }; } /// Returns the exact little-endian header length after validation. @@ -403,14 +389,14 @@ impl DcMetadataSpmData { std::result::Result::Err(error) => return std::result::Result::Err(error), } let actual_bytes = self.wire_byte_len(); - match u32::try_from(actual_bytes) { - std::result::Result::Ok(value) => return std::result::Result::Ok(value), + return match u32::try_from(actual_bytes) { + std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Err(_) => { - return std::result::Result::Err( - crate::DcMetadataSpmModelError::DataLengthExceedsU32 { actual_bytes }, - ); + std::result::Result::Err(crate::DcMetadataSpmModelError::DataLengthExceedsU32 { + actual_bytes, + }) }, - } + }; } /// Serializes the exact content bytes without fetching URL or external-account data. @@ -421,15 +407,11 @@ impl DcMetadataSpmData { std::result::Result::Ok(()) => {}, std::result::Result::Err(error) => return std::result::Result::Err(error), } - match self { - Self::Direct(value) => return std::result::Result::Ok(value.clone()), - Self::Url(value) => { - return std::result::Result::Ok(value.as_bytes().to_vec()); - }, - Self::External(value) => { - return std::result::Result::Ok(value.to_wire_bytes().to_vec()); - }, - } + return match self { + Self::Direct(value) => std::result::Result::Ok(value.clone()), + Self::Url(value) => std::result::Result::Ok(value.as_bytes().to_vec()), + Self::External(value) => std::result::Result::Ok(value.to_wire_bytes().to_vec()), + }; } } @@ -503,56 +485,50 @@ pub enum DcMetadataSpmModelError { impl std::fmt::Display for DcMetadataSpmModelError { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { + return match self { Self::UnknownAccountDiscriminator { value } => { - return write!(formatter, "unknown account discriminator {value}"); + write!(formatter, "unknown account discriminator {value}") }, Self::UnknownEncoding { value } => { - return write!(formatter, "unknown encoding discriminator {value}"); + write!(formatter, "unknown encoding discriminator {value}") }, Self::UnknownCompression { value } => { - return write!(formatter, "unknown compression discriminator {value}"); + write!(formatter, "unknown compression discriminator {value}") }, Self::UnknownFormat { value } => { - return write!(formatter, "unknown format discriminator {value}"); + write!(formatter, "unknown format discriminator {value}") }, Self::UnknownDataSource { value } => { - return write!(formatter, "unknown data-source discriminator {value}"); + write!(formatter, "unknown data-source discriminator {value}") }, Self::UnknownProgramError { value } => { - return write!(formatter, "unknown program metadata error code {value}"); + write!(formatter, "unknown program metadata error code {value}") }, Self::SeedUtf8TooLong { actual_bytes, max_bytes } => { - return write!( - formatter, - "seed UTF-8 length {actual_bytes} exceeds {max_bytes} bytes", - ); + write!(formatter, "seed UTF-8 length {actual_bytes} exceeds {max_bytes} bytes",) }, Self::InvalidSeedLength { actual_bytes, expected_bytes } => { - return write!( + write!( formatter, "seed length {actual_bytes} does not equal {expected_bytes} bytes", - ); + ) }, Self::InvalidSeedUtf8 => { - return formatter.write_str("seed contains invalid UTF-8 before terminal padding"); + formatter.write_str("seed contains invalid UTF-8 before terminal padding") }, Self::InvalidExternalDataLength { actual_bytes, expected_bytes } => { - return write!( + write!( formatter, "external data length {actual_bytes} does not equal {expected_bytes} bytes", - ); + ) }, Self::EmptyData { source } => { - return write!(formatter, "metadata data source {source:?} cannot be empty"); + write!(formatter, "metadata data source {source:?} cannot be empty") }, Self::DataLengthExceedsU32 { actual_bytes } => { - return write!( - formatter, - "metadata data length {actual_bytes} exceeds the u32 wire bound", - ); + write!(formatter, "metadata data length {actual_bytes} exceeds the u32 wire bound",) }, - } + }; } } diff --git a/kb-lib/src/executor.rs b/kb-lib/src/executor.rs index 5e72593..c3358aa 100644 --- a/kb-lib/src/executor.rs +++ b/kb-lib/src/executor.rs @@ -1,5 +1,5 @@ // file: kb-lib/src/executor.rs -// version: 14 +// version: 16 //! Consolidated executor modules. @@ -208,6 +208,30 @@ pub use self::lending::ExLendingKaminoExecutor; pub use self::lending::ExLendingMarginfiV2Executor; /// Exposes the reserved `lock/raydium_lp` executor. pub use self::lock::ExLockRaydiumLpExecutor; +/// Stable Solana Program Metadata `Allocate` operation code. +pub use self::metadata::EX_METADATA_SPM_ALLOCATE_OPERATION; +/// Stable Solana Program Metadata `Close` operation code. +pub use self::metadata::EX_METADATA_SPM_CLOSE_OPERATION; +/// Solana Program Metadata operations requiring explicit destructive approval. +pub use self::metadata::EX_METADATA_SPM_DESTRUCTIVE_OPERATION_CODES; +/// Stable Solana Program Metadata `Extend` operation code. +pub use self::metadata::EX_METADATA_SPM_EXTEND_OPERATION; +/// Stable Solana Program Metadata `Initialize` operation code. +pub use self::metadata::EX_METADATA_SPM_INITIALIZE_OPERATION; +/// Conservative maximum inline Solana Program Metadata content size. +pub use self::metadata::EX_METADATA_SPM_MAX_INLINE_DATA_BYTES; +/// Stable Solana Program Metadata `SetAuthority` operation code. +pub use self::metadata::EX_METADATA_SPM_SET_AUTHORITY_OPERATION; +/// Stable Solana Program Metadata `SetData` operation code. +pub use self::metadata::EX_METADATA_SPM_SET_DATA_OPERATION; +/// Stable Solana Program Metadata `SetImmutable` operation code. +pub use self::metadata::EX_METADATA_SPM_SET_IMMUTABLE_OPERATION; +/// Exact Solana Program Metadata operation inventory. +pub use self::metadata::EX_METADATA_SPM_SUPPORTED_OPERATION_CODES; +/// Stable Solana Program Metadata `Trim` operation code. +pub use self::metadata::EX_METADATA_SPM_TRIM_OPERATION; +/// Stable Solana Program Metadata `Write` operation code. +pub use self::metadata::EX_METADATA_SPM_WRITE_OPERATION; /// Exposes `EX_METAPLEX_TOKEN_METADATA_APPROVE_USE_AUTHORITY_OPERATION`. pub use self::metadata::EX_METAPLEX_TOKEN_METADATA_APPROVE_USE_AUTHORITY_OPERATION; /// Exposes `EX_METAPLEX_TOKEN_METADATA_BURN_OPERATION`. @@ -284,14 +308,38 @@ pub use self::metadata::EX_METAPLEX_TOKEN_METADATA_UTILIZE_OPERATION; pub use self::metadata::EX_METAPLEX_TOKEN_METADATA_VERIFY_OPERATION; /// Exposes the Metaplex Token Metadata executor. pub use self::metadata::ExMetadataMetaplexTokenMetadataExecutor; -/// Reserved `kb-lib.executor.metadata.solana_program_metadata` executor. +/// Typed Solana Program Metadata executor. pub use self::metadata::ExMetadataSolanaProgramMetadataExecutor; /// Exposes the reserved `metadata/spl_name_service` executor. pub use self::metadata::ExMetadataSplNameServiceExecutor; +/// Solana Program Metadata compression intent. +pub use self::metadata::ExMetadataSpmCompression; +/// Solana Program Metadata inline data intent. +pub use self::metadata::ExMetadataSpmDataInput; +/// Solana Program Metadata data-source intent. +pub use self::metadata::ExMetadataSpmDataSource; +/// Solana Program Metadata encoding intent. +pub use self::metadata::ExMetadataSpmEncoding; +/// Complete Solana Program Metadata execution intent. +pub use self::metadata::ExMetadataSpmExecutionIntent; +/// Solana Program Metadata format intent. +pub use self::metadata::ExMetadataSpmFormat; +/// One Solana Program Metadata operation. +pub use self::metadata::ExMetadataSpmOperation; +/// Optional Solana Program Metadata program context. +pub use self::metadata::ExMetadataSpmProgramContext; +/// Solana Program Metadata `SetData` source intent. +pub use self::metadata::ExMetadataSpmSetDataSource; +/// Solana Program Metadata `Write` source intent. +pub use self::metadata::ExMetadataSpmWriteSource; /// Exposes the complete Metaplex Token Metadata execution intent. pub use self::metadata::ExMetaplexTokenMetadataExecutionIntent; /// Exposes one current Metaplex Token Metadata operation. pub use self::metadata::ExMetaplexTokenMetadataOperation; +/// Derives the canonical Solana Program Metadata PDA. +pub use self::metadata::executor_metadata_solana_program_metadata_derive_canonical_pda; +/// Derives the non-canonical Solana Program Metadata PDA. +pub use self::metadata::executor_metadata_solana_program_metadata_derive_non_canonical_pda; /// Exposes the reserved `nft/metaplex_bubblegum` executor. pub use self::nft::ExNftMetaplexBubblegumExecutor; /// Reserved `kb-lib.executor.nft.tensor_cnft` executor. @@ -993,6 +1041,10 @@ pub use self::wallet::ExWalletJupiterApeproSmartWalletExecutor; /// Exposes the reserved `weighted/swap_stabble` executor. pub use self::weighted::ExWeightedSwapStabbleExecutor; +/// Stable Solana Program Metadata executor name. +pub(crate) use self::metadata::EX_METADATA_SPM_EXECUTOR_NAME; +/// Internal Solana Program Metadata prepared-plan builder. +pub(crate) use self::metadata::executor_metadata_solana_program_metadata_build_prepared_plan; /// Internal Metaplex Token Metadata prepared-plan builder. pub(crate) use self::metadata::executor_metaplex_token_metadata_build_prepared_plan; /// Stable operation code for the nonce advance injected by durable transaction assembly. @@ -1078,11 +1130,10 @@ pub(crate) use self::spl::executor_spl_token_build_prepared_plan; mod reserved_executor_tests { #[test] fn every_reserved_executor_is_registered_and_builds_only_an_empty_plan() { - let executors: [&dyn crate::ExApiInstructionExecutor; 104] = [ + let executors: [&dyn crate::ExApiInstructionExecutor; 103] = [ &crate::ExAdapterSaberDecimalWrapperExecutor, &crate::ExAdapterSplTokenWrapExecutor, &crate::ExGovernanceSquadsMultisigExecutor, - &crate::ExMetadataSolanaProgramMetadataExecutor, &crate::ExNftTensorCnftExecutor, &crate::ExPerpetualsPhoenixEternalExecutor, &crate::ExStakingJitoTipDistributionExecutor, @@ -1184,7 +1235,7 @@ mod reserved_executor_tests { &crate::ExWalletJupiterApeproSmartWalletExecutor, &crate::ExWeightedSwapStabbleExecutor, ]; - assert_eq!(executors.len(), 104); + assert_eq!(executors.len(), 103); for executor in executors { assert!(!executor.program_ids().is_empty()); for program_id in executor.program_ids() { diff --git a/kb-lib/src/executor/metadata.rs b/kb-lib/src/executor/metadata.rs index 6c7fcdb..a79182e 100644 --- a/kb-lib/src/executor/metadata.rs +++ b/kb-lib/src/executor/metadata.rs @@ -1,5 +1,5 @@ // file: kb-lib/src/executor/metadata.rs -// version: 8 +// version: 9 //! `metadata` executor family. @@ -87,9 +87,61 @@ pub use self::metaplex_token_metadata::ExMetadataMetaplexTokenMetadataExecutor; pub use self::metaplex_token_metadata::ExMetaplexTokenMetadataExecutionIntent; /// Exposes the Metaplex Token Metadata operation intent. pub use self::metaplex_token_metadata::ExMetaplexTokenMetadataOperation; -/// Exposes the reserved `metadata/solana_program_metadata` executor. +/// Stable Solana Program Metadata `Allocate` operation code. +pub use self::solana_program_metadata::EX_METADATA_SPM_ALLOCATE_OPERATION; +/// Stable Solana Program Metadata `Close` operation code. +pub use self::solana_program_metadata::EX_METADATA_SPM_CLOSE_OPERATION; +/// Solana Program Metadata operations requiring explicit destructive approval. +pub use self::solana_program_metadata::EX_METADATA_SPM_DESTRUCTIVE_OPERATION_CODES; +/// Stable Solana Program Metadata `Extend` operation code. +pub use self::solana_program_metadata::EX_METADATA_SPM_EXTEND_OPERATION; +/// Stable Solana Program Metadata `Initialize` operation code. +pub use self::solana_program_metadata::EX_METADATA_SPM_INITIALIZE_OPERATION; +/// Conservative maximum inline Solana Program Metadata content size. +pub use self::solana_program_metadata::EX_METADATA_SPM_MAX_INLINE_DATA_BYTES; +/// Stable Solana Program Metadata `SetAuthority` operation code. +pub use self::solana_program_metadata::EX_METADATA_SPM_SET_AUTHORITY_OPERATION; +/// Stable Solana Program Metadata `SetData` operation code. +pub use self::solana_program_metadata::EX_METADATA_SPM_SET_DATA_OPERATION; +/// Stable Solana Program Metadata `SetImmutable` operation code. +pub use self::solana_program_metadata::EX_METADATA_SPM_SET_IMMUTABLE_OPERATION; +/// Exact Solana Program Metadata operation inventory. +pub use self::solana_program_metadata::EX_METADATA_SPM_SUPPORTED_OPERATION_CODES; +/// Stable Solana Program Metadata `Trim` operation code. +pub use self::solana_program_metadata::EX_METADATA_SPM_TRIM_OPERATION; +/// Stable Solana Program Metadata `Write` operation code. +pub use self::solana_program_metadata::EX_METADATA_SPM_WRITE_OPERATION; +/// Typed Solana Program Metadata executor. pub use self::solana_program_metadata::ExMetadataSolanaProgramMetadataExecutor; +/// Solana Program Metadata compression intent. +pub use self::solana_program_metadata::ExMetadataSpmCompression; +/// Solana Program Metadata inline data intent. +pub use self::solana_program_metadata::ExMetadataSpmDataInput; +/// Solana Program Metadata data-source intent. +pub use self::solana_program_metadata::ExMetadataSpmDataSource; +/// Solana Program Metadata encoding intent. +pub use self::solana_program_metadata::ExMetadataSpmEncoding; +/// Complete Solana Program Metadata execution intent. +pub use self::solana_program_metadata::ExMetadataSpmExecutionIntent; +/// Solana Program Metadata format intent. +pub use self::solana_program_metadata::ExMetadataSpmFormat; +/// One Solana Program Metadata operation. +pub use self::solana_program_metadata::ExMetadataSpmOperation; +/// Optional Solana Program Metadata program context. +pub use self::solana_program_metadata::ExMetadataSpmProgramContext; +/// Solana Program Metadata `SetData` source intent. +pub use self::solana_program_metadata::ExMetadataSpmSetDataSource; +/// Solana Program Metadata `Write` source intent. +pub use self::solana_program_metadata::ExMetadataSpmWriteSource; +/// Derives the canonical Solana Program Metadata PDA. +pub use self::solana_program_metadata::executor_metadata_solana_program_metadata_derive_canonical_pda; +/// Derives the non-canonical Solana Program Metadata PDA. +pub use self::solana_program_metadata::executor_metadata_solana_program_metadata_derive_non_canonical_pda; /// Exposes the reserved `metadata/spl_name_service` executor. pub use self::spl_name_service::ExMetadataSplNameServiceExecutor; pub(crate) use self::metaplex_token_metadata::executor_metaplex_token_metadata_build_prepared_plan; +/// Stable Solana Program Metadata executor name. +pub(crate) use self::solana_program_metadata::EX_METADATA_SPM_EXECUTOR_NAME; +/// Internal Solana Program Metadata prepared-plan builder. +pub(crate) use self::solana_program_metadata::executor_metadata_solana_program_metadata_build_prepared_plan; diff --git a/kb-lib/src/executor/metadata/solana_program_metadata.rs b/kb-lib/src/executor/metadata/solana_program_metadata.rs index 8c1a6c2..6b94a69 100644 --- a/kb-lib/src/executor/metadata/solana_program_metadata.rs +++ b/kb-lib/src/executor/metadata/solana_program_metadata.rs @@ -1,49 +1,65 @@ // file: kb-lib/src/executor/metadata/solana_program_metadata.rs -// version: 1 +// version: 2 -//! Reserved executor for `metadata_solana_program_metadata`. +//! Typed executor for Solana Program Metadata. -/// Reserved executor for the `metadata_solana_program_metadata` program surface. -#[derive(Clone, Debug, Default)] -pub struct ExMetadataSolanaProgramMetadataExecutor; +mod builder; +mod constants; +mod executor; +mod intent; -impl crate::ExApiInstructionExecutor for crate::ExMetadataSolanaProgramMetadataExecutor { - fn executor_name(&self) -> &'static str { - return "kb-lib.executor.metadata.solana_program_metadata"; - } +/// Derives the canonical Metadata PDA for one program and seed. +pub use self::builder::executor_metadata_solana_program_metadata_derive_canonical_pda; +/// Derives the non-canonical Metadata PDA for one program, authority and seed. +pub use self::builder::executor_metadata_solana_program_metadata_derive_non_canonical_pda; +/// Stable operation code for `Allocate`. +pub use self::constants::EX_METADATA_SPM_ALLOCATE_OPERATION; +/// Stable operation code for `Close`. +pub use self::constants::EX_METADATA_SPM_CLOSE_OPERATION; +/// Operations requiring explicit destructive-operation approval. +pub use self::constants::EX_METADATA_SPM_DESTRUCTIVE_OPERATION_CODES; +/// Stable operation code for `Extend`. +pub use self::constants::EX_METADATA_SPM_EXTEND_OPERATION; +/// Stable operation code for `Initialize`. +pub use self::constants::EX_METADATA_SPM_INITIALIZE_OPERATION; +/// Conservative maximum inline content accepted by Solana Program Metadata builders. +pub use self::constants::EX_METADATA_SPM_MAX_INLINE_DATA_BYTES; +/// Stable operation code for `SetAuthority`. +pub use self::constants::EX_METADATA_SPM_SET_AUTHORITY_OPERATION; +/// Stable operation code for `SetData`. +pub use self::constants::EX_METADATA_SPM_SET_DATA_OPERATION; +/// Stable operation code for `SetImmutable`. +pub use self::constants::EX_METADATA_SPM_SET_IMMUTABLE_OPERATION; +/// Exact current operation inventory implemented by the executor. +pub use self::constants::EX_METADATA_SPM_SUPPORTED_OPERATION_CODES; +/// Stable operation code for `Trim`. +pub use self::constants::EX_METADATA_SPM_TRIM_OPERATION; +/// Stable operation code for `Write`. +pub use self::constants::EX_METADATA_SPM_WRITE_OPERATION; +/// Typed Solana Program Metadata executor. +pub use self::executor::ExMetadataSolanaProgramMetadataExecutor; +/// Compression written by execution intents. +pub use self::intent::ExMetadataSpmCompression; +/// Exact inline content accepted by metadata instructions. +pub use self::intent::ExMetadataSpmDataInput; +/// On-chain data-source discriminator. +pub use self::intent::ExMetadataSpmDataSource; +/// Encoding written by execution intents. +pub use self::intent::ExMetadataSpmEncoding; +/// Complete Solana Program Metadata execution intent. +pub use self::intent::ExMetadataSpmExecutionIntent; +/// Structured content format written by execution intents. +pub use self::intent::ExMetadataSpmFormat; +/// One exact Solana Program Metadata operation. +pub use self::intent::ExMetadataSpmOperation; +/// Optional program-upgrade-authority validation context. +pub use self::intent::ExMetadataSpmProgramContext; +/// Source used by `SetData`. +pub use self::intent::ExMetadataSpmSetDataSource; +/// Source used by `Write`. +pub use self::intent::ExMetadataSpmWriteSource; - fn executor_version(&self) -> &'static str { - return env!("CARGO_PKG_VERSION"); - } - - fn program_ids(&self) -> &'static [&'static str] { - return &[kb_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID]; - } - - fn supports_request( - &self, - request: &crate::ExApiExecutionRequest, - ) -> crate::ExApiExecutionSupport { - if crate::ExApiInstructionExecutor::handles_program_id(self, &request.program_id) { - return crate::ExApiExecutionSupport::Maybe; - } - return crate::ExApiExecutionSupport::No; - } - - fn build_plan( - &self, - _request: &crate::ExApiExecutionRequest, - ) -> kb_core::Result { - let payload_json = match crate::executor_api_serialize_payload_json( - &serde_json::json!({"status":"reserved_executor","surface":"metadata_solana_program_metadata"}), - ) { - std::result::Result::Ok(serialized) => serialized, - std::result::Result::Err(error) => return std::result::Result::Err(error), - }; - return std::result::Result::Ok(crate::ExApiExecutionPlan { - executor_name: "kb-lib.executor.metadata.solana_program_metadata".to_string(), - instruction_count: 0, - payload_json, - }); - } -} +/// Crate-root access to the exact prepared-plan builder. +pub(crate) use self::builder::executor_metadata_solana_program_metadata_build_prepared_plan; +/// Stable executor name. +pub(crate) use self::constants::EX_METADATA_SPM_EXECUTOR_NAME; diff --git a/kb-lib/src/executor/metadata/solana_program_metadata/builder.rs b/kb-lib/src/executor/metadata/solana_program_metadata/builder.rs new file mode 100644 index 0000000..1d42c12 --- /dev/null +++ b/kb-lib/src/executor/metadata/solana_program_metadata/builder.rs @@ -0,0 +1,1270 @@ +// file: kb-lib/src/executor/metadata/solana_program_metadata/builder.rs +// version: 1 + +//! Exact Solana Program Metadata builders and safety integration. + +use std::str::FromStr; // rust-rules: trait-import + +struct BuiltOperation { + operation_code: &'static str, + authority: crate::MdPubkey, + instruction: crate::ExApiPlannedInstruction, +} + +/// Derives the canonical Metadata PDA for one program and seed. +pub fn executor_metadata_solana_program_metadata_derive_canonical_pda( + program: &crate::MdPubkey, + seed: &[u8], +) -> kb_core::Result { + let program_key = match parse_pubkey(program, "program") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let metadata_program = match metadata_program_id() { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + if seed.len() != crate::DC_METADATA_SPM_SEED_BYTES { + return std::result::Result::Err(kb_core::Error::new( + "execution_metadata_spm_seed_length_invalid", + format!( + "Solana Program Metadata seed requires {} bytes, received {}", + crate::DC_METADATA_SPM_SEED_BYTES, + seed.len() + ), + )); + } + let (address, _) = solana_pubkey::Pubkey::find_program_address( + &[program_key.as_ref(), seed], + &metadata_program, + ); + return std::result::Result::Ok(crate::MdPubkey(address.to_string())); +} + +/// Derives the non-canonical Metadata PDA for one program, authority and seed. +pub fn executor_metadata_solana_program_metadata_derive_non_canonical_pda( + program: &crate::MdPubkey, + authority: &crate::MdPubkey, + seed: &[u8], +) -> kb_core::Result { + let program_key = match parse_pubkey(program, "program") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let authority_key = match parse_pubkey(authority, "authority") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let metadata_program = match metadata_program_id() { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + if seed.len() != crate::DC_METADATA_SPM_SEED_BYTES { + return std::result::Result::Err(kb_core::Error::new( + "execution_metadata_spm_seed_length_invalid", + format!( + "Solana Program Metadata seed requires {} bytes, received {}", + crate::DC_METADATA_SPM_SEED_BYTES, + seed.len() + ), + )); + } + let (address, _) = solana_pubkey::Pubkey::find_program_address( + &[program_key.as_ref(), authority_key.as_ref(), seed], + &metadata_program, + ); + return std::result::Result::Ok(crate::MdPubkey(address.to_string())); +} + +pub(crate) fn executor_metadata_solana_program_metadata_build_prepared_plan( + intent: &crate::ExMetadataSpmExecutionIntent, +) -> kb_core::Result { + if intent.intent_id.trim().is_empty() { + return std::result::Result::Err(kb_core::Error::new( + "execution_metadata_spm_intent_id_empty", + "Solana Program Metadata execution intent id must not be empty", + )); + } + if intent.policy.simulation != crate::ExApiExecutionSimulationPolicy::Required { + return std::result::Result::Err(kb_core::Error::new( + "execution_metadata_spm_simulation_required", + "Solana Program Metadata execution requires RPC simulation", + )); + } + let validation = &intent.policy.post_execution_validation; + if !validation.canonical_insert_required + || !validation.core_extraction_required + || !validation.decode_replay_required + || !validation.materialization_required + { + return std::result::Result::Err(kb_core::Error::new( + "execution_metadata_spm_post_validation_required", + "Solana Program Metadata execution requires canonical insert, core extraction, decode replay and materialization", + )); + } + if intent.operation.requires_explicit_approval() && !intent.allow_destructive_operation { + return std::result::Result::Err(kb_core::Error::new( + "execution_metadata_spm_destructive_approval_required", + format!( + "operation {} requires explicit destructive-operation approval", + intent.operation.operation_code() + ), + )); + } + let fee_payer_key = match parse_pubkey(&intent.fee_payer, "fee_payer") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let built = match build_operation(&intent.operation) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let authority_key = match parse_pubkey(&built.authority, "authority") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let mut required_signers = std::vec![crate::ExApiRequiredSigner { + pubkey: intent.fee_payer.clone(), + role: "fee_payer".to_string(), + }]; + if authority_key != fee_payer_key { + required_signers.push(crate::ExApiRequiredSigner { + pubkey: built.authority.clone(), + role: "metadata_authority".to_string(), + }); + } + let plan = crate::ExApiPreparedExecutionPlan { + executor_name: crate::EX_METADATA_SPM_EXECUTOR_NAME.to_string(), + executor_version: env!("CARGO_PKG_VERSION").to_string(), + intent_id: intent.intent_id.clone(), + operation_code: built.operation_code.to_string(), + fee_payer: intent.fee_payer.clone(), + instructions: std::vec![built.instruction], + required_signers, + policy: intent.policy.clone(), + requested_spend_lamports: 0, + requested_compute_unit_price_micro_lamports: std::option::Option::None, + }; + let checker = crate::ExSafetyChecker; + let evaluation = match checker.evaluate_prepared_plan(&plan) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + if evaluation.decision == crate::ExSafetyDecision::Deny { + let detail = match evaluation.violations.first() { + std::option::Option::Some(violation) => { + format!("{}: {}", violation.code, violation.message) + }, + std::option::Option::None => { + "common safety policy denied the prepared plan".to_string() + }, + }; + return std::result::Result::Err(kb_core::Error::new( + "execution_metadata_spm_safety_denied", + detail, + )); + } + return std::result::Result::Ok(plan); +} + +fn build_operation(operation: &crate::ExMetadataSpmOperation) -> kb_core::Result { + return match operation { + crate::ExMetadataSpmOperation::Write { buffer, authority, offset, source } => { + build_write(buffer, authority, *offset, source) + }, + crate::ExMetadataSpmOperation::Initialize { + metadata, + authority, + program, + program_data, + canonical, + seed, + encoding, + compression, + format, + data_source, + data, + allocate_account, + } => build_initialize( + metadata, + authority, + program, + program_data.as_ref(), + *canonical, + seed, + *encoding, + *compression, + *format, + *data_source, + data.as_ref(), + *allocate_account, + ), + crate::ExMetadataSpmOperation::SetAuthority { + account, + authority, + new_authority, + program_context, + } => build_set_authority( + account, + authority, + new_authority.as_ref(), + program_context.as_ref(), + ), + crate::ExMetadataSpmOperation::SetData { + metadata, + authority, + encoding, + compression, + format, + source, + program_context, + } => build_set_data( + metadata, + authority, + *encoding, + *compression, + *format, + source, + program_context.as_ref(), + ), + crate::ExMetadataSpmOperation::SetImmutable { metadata, authority, program_context } => { + build_set_immutable(metadata, authority, program_context.as_ref()) + }, + crate::ExMetadataSpmOperation::Trim { + account, + authority, + destination, + program_context, + } => build_trim(account, authority, destination, program_context.as_ref()), + crate::ExMetadataSpmOperation::Close { + account, + authority, + destination, + program_context, + } => build_close(account, authority, destination, program_context.as_ref()), + crate::ExMetadataSpmOperation::Allocate { + buffer, + authority, + seed, + program_context, + canonical, + allocate_account, + } => build_allocate( + buffer, + authority, + seed.as_deref(), + program_context.as_ref(), + *canonical, + *allocate_account, + ), + crate::ExMetadataSpmOperation::Extend { + account, + authority, + length, + program_context, + } => build_extend(account, authority, *length, program_context.as_ref()), + }; +} + +fn build_write( + buffer: &crate::MdPubkey, + authority: &crate::MdPubkey, + offset: u32, + source: &crate::ExMetadataSpmWriteSource, +) -> kb_core::Result { + let buffer_key = match parse_pubkey(buffer, "buffer") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let authority_key = match parse_pubkey(authority, "authority") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let mut data = std::vec![0]; + data.extend_from_slice(&offset.to_le_bytes()); + let source_account = match source { + crate::ExMetadataSpmWriteSource::Inline { data: inline } => { + if inline.is_empty() { + return std::result::Result::Err(kb_core::Error::new( + "execution_metadata_spm_write_data_empty", + "Write inline data must not be empty", + )); + } + if let std::result::Result::Err(error) = validate_inline_length(inline.len(), "write") { + return std::result::Result::Err(error); + } + data.extend_from_slice(inline); + optional_placeholder_account(false) + }, + crate::ExMetadataSpmWriteSource::Buffer { source_buffer } => { + let source_key = match parse_pubkey(source_buffer, "source_buffer") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + if source_key == buffer_key { + return std::result::Result::Err(kb_core::Error::new( + "execution_metadata_spm_write_source_same_as_destination", + "Write source Buffer must differ from the destination Buffer", + )); + } + planned_account(source_buffer.clone(), false, false) + }, + }; + return std::result::Result::Ok(BuiltOperation { + operation_code: crate::EX_METADATA_SPM_WRITE_OPERATION, + authority: authority.clone(), + instruction: instruction( + crate::EX_METADATA_SPM_WRITE_OPERATION, + std::vec![ + planned_account(crate::MdPubkey(buffer_key.to_string()), false, true), + planned_account(crate::MdPubkey(authority_key.to_string()), true, false), + source_account, + ], + data, + ), + }); +} + +#[allow(clippy::too_many_arguments)] +fn build_initialize( + metadata: &crate::MdPubkey, + authority: &crate::MdPubkey, + program: &crate::MdPubkey, + program_data: std::option::Option<&crate::MdPubkey>, + canonical: bool, + seed: &[u8], + encoding: crate::ExMetadataSpmEncoding, + compression: crate::ExMetadataSpmCompression, + format: crate::ExMetadataSpmFormat, + declared_data_source: crate::ExMetadataSpmDataSource, + inline_data: std::option::Option<&crate::ExMetadataSpmDataInput>, + allocate_account: bool, +) -> kb_core::Result { + let metadata_key = match parse_pubkey(metadata, "metadata") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let authority_key = match parse_pubkey(authority, "authority") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let program_key = match parse_pubkey(program, "program") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + if let std::result::Result::Err(error) = validate_seed(seed) { + return std::result::Result::Err(error); + } + let expected = if canonical { + crate::executor_metadata_solana_program_metadata_derive_canonical_pda(program, seed) + } else { + crate::executor_metadata_solana_program_metadata_derive_non_canonical_pda( + program, authority, seed, + ) + }; + let expected = match expected { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + if expected.0 != metadata_key.to_string() { + return std::result::Result::Err(kb_core::Error::new( + "execution_metadata_spm_metadata_pda_mismatch", + format!("metadata account {} does not match expected PDA {}", metadata.0, expected.0), + )); + } + if allocate_account && inline_data.is_none() { + return std::result::Result::Err(kb_core::Error::new( + "execution_metadata_spm_initialize_data_missing", + "Initialize with System Program allocation requires inline data", + )); + } + let mut data = std::vec![1]; + data.extend_from_slice(seed); + data.push(encoding.wire_value()); + data.push(compression.wire_value()); + data.push(format.wire_value()); + data.push(declared_data_source.wire_value()); + if let std::option::Option::Some(value) = inline_data { + if value.data_source() != declared_data_source { + return std::result::Result::Err(kb_core::Error::new( + "execution_metadata_spm_data_source_mismatch", + "Initialize inline data does not match the declared data source", + )); + } + let wire = match encode_data_input(value) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + if let std::result::Result::Err(error) = + validate_inline_length(data.len().saturating_add(wire.len()), "initialize") + { + return std::result::Result::Err(error); + } + data.extend_from_slice(&wire); + } + let program_data_account = match program_data { + std::option::Option::Some(value) => { + match parse_pubkey(value, "program_data") { + std::result::Result::Ok(_) => {}, + std::result::Result::Err(error) => return std::result::Result::Err(error), + } + planned_account(value.clone(), false, false) + }, + std::option::Option::None => optional_placeholder_account(false), + }; + let system_account = if allocate_account { + planned_account( + crate::MdPubkey(kb_program_ids::SYSTEM_PROGRAM_ID.to_string()), + false, + false, + ) + } else { + optional_placeholder_account(false) + }; + return std::result::Result::Ok(BuiltOperation { + operation_code: crate::EX_METADATA_SPM_INITIALIZE_OPERATION, + authority: authority.clone(), + instruction: instruction( + crate::EX_METADATA_SPM_INITIALIZE_OPERATION, + std::vec![ + planned_account(crate::MdPubkey(metadata_key.to_string()), false, true), + planned_account(crate::MdPubkey(authority_key.to_string()), true, false), + planned_account(crate::MdPubkey(program_key.to_string()), false, false), + program_data_account, + system_account, + ], + data, + ), + }); +} + +fn build_set_authority( + account: &crate::MdPubkey, + authority: &crate::MdPubkey, + new_authority: std::option::Option<&crate::MdPubkey>, + program_context: std::option::Option<&crate::ExMetadataSpmProgramContext>, +) -> kb_core::Result { + if let std::result::Result::Err(error) = + validate_pubkeys(&[(account, "account"), (authority, "authority")]) + { + return std::result::Result::Err(error); + } + let mut data = std::vec![2]; + match new_authority { + std::option::Option::Some(value) => { + let key = match parse_pubkey(value, "new_authority") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + data.push(1); + data.extend_from_slice(&key.to_bytes()); + }, + std::option::Option::None => data.push(0), + } + let (program_account, program_data_account) = match context_accounts(program_context) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return std::result::Result::Ok(BuiltOperation { + operation_code: crate::EX_METADATA_SPM_SET_AUTHORITY_OPERATION, + authority: authority.clone(), + instruction: instruction( + crate::EX_METADATA_SPM_SET_AUTHORITY_OPERATION, + std::vec![ + planned_account(account.clone(), false, true), + planned_account(authority.clone(), true, false), + program_account, + program_data_account, + ], + data, + ), + }); +} + +fn build_set_data( + metadata: &crate::MdPubkey, + authority: &crate::MdPubkey, + encoding: crate::ExMetadataSpmEncoding, + compression: crate::ExMetadataSpmCompression, + format: crate::ExMetadataSpmFormat, + source: &crate::ExMetadataSpmSetDataSource, + program_context: std::option::Option<&crate::ExMetadataSpmProgramContext>, +) -> kb_core::Result { + if let std::result::Result::Err(error) = + validate_pubkeys(&[(metadata, "metadata"), (authority, "authority")]) + { + return std::result::Result::Err(error); + } + let mut data = + std::vec![3, encoding.wire_value(), compression.wire_value(), format.wire_value()]; + let buffer_account = match source { + crate::ExMetadataSpmSetDataSource::PreserveExisting => optional_placeholder_account(true), + crate::ExMetadataSpmSetDataSource::Inline { data: inline } => { + data.push(inline.data_source().wire_value()); + let wire = match encode_data_input(inline) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + if let std::result::Result::Err(error) = + validate_inline_length(data.len().saturating_add(wire.len()), "set_data") + { + return std::result::Result::Err(error); + } + data.extend_from_slice(&wire); + optional_placeholder_account(true) + }, + crate::ExMetadataSpmSetDataSource::Buffer { buffer, data_source } => { + if let std::result::Result::Err(error) = validate_pubkeys(&[(buffer, "buffer")]) { + return std::result::Result::Err(error); + } + if buffer == metadata { + return std::result::Result::Err(kb_core::Error::new( + "execution_metadata_spm_set_data_buffer_same_as_metadata", + "SetData source Buffer must differ from the Metadata account", + )); + } + data.push(data_source.wire_value()); + planned_account(buffer.clone(), false, true) + }, + }; + let (program_account, program_data_account) = match context_accounts(program_context) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return std::result::Result::Ok(BuiltOperation { + operation_code: crate::EX_METADATA_SPM_SET_DATA_OPERATION, + authority: authority.clone(), + instruction: instruction( + crate::EX_METADATA_SPM_SET_DATA_OPERATION, + std::vec![ + planned_account(metadata.clone(), false, true), + planned_account(authority.clone(), true, false), + buffer_account, + program_account, + program_data_account, + ], + data, + ), + }); +} + +fn build_set_immutable( + metadata: &crate::MdPubkey, + authority: &crate::MdPubkey, + program_context: std::option::Option<&crate::ExMetadataSpmProgramContext>, +) -> kb_core::Result { + if let std::result::Result::Err(error) = + validate_pubkeys(&[(metadata, "metadata"), (authority, "authority")]) + { + return std::result::Result::Err(error); + } + let (program_account, program_data_account) = match context_accounts(program_context) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return std::result::Result::Ok(BuiltOperation { + operation_code: crate::EX_METADATA_SPM_SET_IMMUTABLE_OPERATION, + authority: authority.clone(), + instruction: instruction( + crate::EX_METADATA_SPM_SET_IMMUTABLE_OPERATION, + std::vec![ + planned_account(metadata.clone(), false, true), + planned_account(authority.clone(), true, false), + program_account, + program_data_account, + ], + std::vec![4], + ), + }); +} + +fn build_trim( + account: &crate::MdPubkey, + authority: &crate::MdPubkey, + destination: &crate::MdPubkey, + program_context: std::option::Option<&crate::ExMetadataSpmProgramContext>, +) -> kb_core::Result { + if let std::result::Result::Err(error) = + validate_destination_contract(account, authority, destination) + { + return std::result::Result::Err(error); + } + let (program_account, program_data_account) = match context_accounts(program_context) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return std::result::Result::Ok(BuiltOperation { + operation_code: crate::EX_METADATA_SPM_TRIM_OPERATION, + authority: authority.clone(), + instruction: instruction( + crate::EX_METADATA_SPM_TRIM_OPERATION, + std::vec![ + planned_account(account.clone(), false, true), + planned_account(authority.clone(), true, false), + program_account, + program_data_account, + planned_account(destination.clone(), false, true), + planned_account( + crate::MdPubkey(kb_program_ids::SYSVAR_RENT_PROGRAM_ID.to_string()), + false, + false, + ), + ], + std::vec![5], + ), + }); +} + +fn build_close( + account: &crate::MdPubkey, + authority: &crate::MdPubkey, + destination: &crate::MdPubkey, + program_context: std::option::Option<&crate::ExMetadataSpmProgramContext>, +) -> kb_core::Result { + if let std::result::Result::Err(error) = + validate_destination_contract(account, authority, destination) + { + return std::result::Result::Err(error); + } + let (program_account, program_data_account) = match context_accounts(program_context) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return std::result::Result::Ok(BuiltOperation { + operation_code: crate::EX_METADATA_SPM_CLOSE_OPERATION, + authority: authority.clone(), + instruction: instruction( + crate::EX_METADATA_SPM_CLOSE_OPERATION, + std::vec![ + planned_account(account.clone(), false, true), + planned_account(authority.clone(), true, false), + program_account, + program_data_account, + planned_account(destination.clone(), false, true), + ], + std::vec![6], + ), + }); +} + +fn build_allocate( + buffer: &crate::MdPubkey, + authority: &crate::MdPubkey, + seed: std::option::Option<&[u8]>, + program_context: std::option::Option<&crate::ExMetadataSpmProgramContext>, + canonical: bool, + allocate_account: bool, +) -> kb_core::Result { + if let std::result::Result::Err(error) = + validate_pubkeys(&[(buffer, "buffer"), (authority, "authority")]) + { + return std::result::Result::Err(error); + } + let mut data = std::vec![7]; + match seed { + std::option::Option::Some(value) => { + if let std::result::Result::Err(error) = validate_seed(value) { + return std::result::Result::Err(error); + } + let context = match program_context { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(kb_core::Error::new( + "execution_metadata_spm_allocate_program_context_required", + "PDA Buffer allocation requires an executable program context", + )); + }, + }; + let expected = if canonical { + crate::executor_metadata_solana_program_metadata_derive_canonical_pda( + &context.program, + value, + ) + } else { + crate::executor_metadata_solana_program_metadata_derive_non_canonical_pda( + &context.program, + authority, + value, + ) + }; + let expected = match expected { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + if expected != *buffer { + return std::result::Result::Err(kb_core::Error::new( + "execution_metadata_spm_allocate_buffer_pda_mismatch", + format!( + "buffer account {} does not match expected PDA {}", + buffer.0, expected.0 + ), + )); + } + data.extend_from_slice(value); + }, + std::option::Option::None => { + if canonical { + return std::result::Result::Err(kb_core::Error::new( + "execution_metadata_spm_allocate_keypair_canonical_invalid", + "keypair Buffer allocation cannot be canonical", + )); + } + if buffer != authority { + return std::result::Result::Err(kb_core::Error::new( + "execution_metadata_spm_allocate_keypair_authority_mismatch", + "keypair Buffer allocation requires the Buffer account and authority to be identical", + )); + } + }, + } + let (program_account, program_data_account) = match context_accounts(program_context) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let system_account = if allocate_account { + planned_account( + crate::MdPubkey(kb_program_ids::SYSTEM_PROGRAM_ID.to_string()), + false, + false, + ) + } else { + optional_placeholder_account(false) + }; + return std::result::Result::Ok(BuiltOperation { + operation_code: crate::EX_METADATA_SPM_ALLOCATE_OPERATION, + authority: authority.clone(), + instruction: instruction( + crate::EX_METADATA_SPM_ALLOCATE_OPERATION, + std::vec![ + planned_account(buffer.clone(), false, true), + planned_account(authority.clone(), true, false), + program_account, + program_data_account, + system_account, + ], + data, + ), + }); +} + +fn build_extend( + account: &crate::MdPubkey, + authority: &crate::MdPubkey, + length: u16, + program_context: std::option::Option<&crate::ExMetadataSpmProgramContext>, +) -> kb_core::Result { + if let std::result::Result::Err(error) = + validate_pubkeys(&[(account, "account"), (authority, "authority")]) + { + return std::result::Result::Err(error); + } + if length == 0 { + return std::result::Result::Err(kb_core::Error::new( + "execution_metadata_spm_extend_length_zero", + "Extend length must be positive", + )); + } + let (program_account, program_data_account) = match context_accounts(program_context) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let mut data = std::vec![8]; + data.extend_from_slice(&length.to_le_bytes()); + return std::result::Result::Ok(BuiltOperation { + operation_code: crate::EX_METADATA_SPM_EXTEND_OPERATION, + authority: authority.clone(), + instruction: instruction( + crate::EX_METADATA_SPM_EXTEND_OPERATION, + std::vec![ + planned_account(account.clone(), false, true), + planned_account(authority.clone(), true, false), + program_account, + program_data_account, + ], + data, + ), + }); +} + +fn encode_data_input(input: &crate::ExMetadataSpmDataInput) -> kb_core::Result> { + return match input { + crate::ExMetadataSpmDataInput::Direct { bytes } => { + if bytes.is_empty() { + return std::result::Result::Err(kb_core::Error::new( + "execution_metadata_spm_direct_data_empty", + "direct metadata content must not be empty", + )); + } + std::result::Result::Ok(bytes.clone()) + }, + crate::ExMetadataSpmDataInput::Url { url } => { + if url.is_empty() { + return std::result::Result::Err(kb_core::Error::new( + "execution_metadata_spm_url_empty", + "metadata URL must not be empty", + )); + } + std::result::Result::Ok(url.as_bytes().to_vec()) + }, + crate::ExMetadataSpmDataInput::External { address, offset, length } => { + let address_key = match parse_pubkey(address, "external_address") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + if *length == std::option::Option::Some(0) { + return std::result::Result::Err(kb_core::Error::new( + "execution_metadata_spm_external_length_zero", + "external metadata length must be absent or positive", + )); + } + let mut wire = std::vec::Vec::with_capacity(crate::DC_METADATA_SPM_EXTERNAL_DATA_BYTES); + wire.extend_from_slice(&address_key.to_bytes()); + wire.extend_from_slice(&offset.to_le_bytes()); + let wire_length = match length { + std::option::Option::Some(value) => *value, + std::option::Option::None => 0, + }; + wire.extend_from_slice(&wire_length.to_le_bytes()); + std::result::Result::Ok(wire) + }, + }; +} + +fn context_accounts( + context: std::option::Option<&crate::ExMetadataSpmProgramContext>, +) -> kb_core::Result<(crate::ExApiPlannedAccount, crate::ExApiPlannedAccount)> { + return match context { + std::option::Option::Some(value) => { + if let std::result::Result::Err(error) = + validate_pubkeys(&[(&value.program, "program")]) + { + return std::result::Result::Err(error); + } + let program_data = match &value.program_data { + std::option::Option::Some(program_data) => { + if let std::result::Result::Err(error) = + validate_pubkeys(&[(program_data, "program_data")]) + { + return std::result::Result::Err(error); + } + planned_account(program_data.clone(), false, false) + }, + std::option::Option::None => optional_placeholder_account(false), + }; + std::result::Result::Ok(( + planned_account(value.program.clone(), false, false), + program_data, + )) + }, + std::option::Option::None => std::result::Result::Ok(( + optional_placeholder_account(false), + optional_placeholder_account(false), + )), + }; +} + +fn validate_destination_contract( + account: &crate::MdPubkey, + authority: &crate::MdPubkey, + destination: &crate::MdPubkey, +) -> kb_core::Result<()> { + if let std::result::Result::Err(error) = validate_pubkeys(&[ + (account, "account"), + (authority, "authority"), + (destination, "destination"), + ]) { + return std::result::Result::Err(error); + } + if account == destination { + return std::result::Result::Err(kb_core::Error::new( + "execution_metadata_spm_destination_same_as_account", + "destination must differ from the account being trimmed or closed", + )); + } + return std::result::Result::Ok(()); +} + +fn validate_seed(seed: &[u8]) -> kb_core::Result<()> { + if seed.len() != crate::DC_METADATA_SPM_SEED_BYTES { + return std::result::Result::Err(kb_core::Error::new( + "execution_metadata_spm_seed_length_invalid", + format!( + "Solana Program Metadata seed requires {} bytes, received {}", + crate::DC_METADATA_SPM_SEED_BYTES, + seed.len() + ), + )); + } + return std::result::Result::Ok(()); +} + +fn validate_inline_length(length: usize, operation: &str) -> kb_core::Result<()> { + if length > crate::EX_METADATA_SPM_MAX_INLINE_DATA_BYTES { + return std::result::Result::Err(kb_core::Error::new( + "execution_metadata_spm_inline_data_too_large", + format!( + "{operation} instruction payload exceeds {} bytes", + crate::EX_METADATA_SPM_MAX_INLINE_DATA_BYTES + ), + )); + } + return std::result::Result::Ok(()); +} + +fn validate_pubkeys(values: &[(&crate::MdPubkey, &str)]) -> kb_core::Result<()> { + for (value, role) in values { + match parse_pubkey(value, role) { + std::result::Result::Ok(_) => {}, + std::result::Result::Err(error) => return std::result::Result::Err(error), + } + } + return std::result::Result::Ok(()); +} + +fn parse_pubkey(value: &crate::MdPubkey, role: &str) -> kb_core::Result { + return match solana_pubkey::Pubkey::from_str(&value.0) { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::new( + "execution_metadata_spm_pubkey_invalid", + format!("invalid {role} public key: {error}"), + )), + }; +} + +fn metadata_program_id() -> kb_core::Result { + return match solana_pubkey::Pubkey::from_str( + kb_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID, + ) { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::new( + "execution_metadata_spm_program_id_invalid", + error.to_string(), + )), + }; +} + +fn optional_placeholder_account(writable: bool) -> crate::ExApiPlannedAccount { + return planned_account( + crate::MdPubkey(kb_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID.to_string()), + false, + writable, + ); +} + +fn planned_account( + pubkey: crate::MdPubkey, + is_signer: bool, + is_writable: bool, +) -> crate::ExApiPlannedAccount { + return crate::ExApiPlannedAccount { pubkey, is_signer, is_writable }; +} + +fn instruction( + operation_code: &str, + accounts: std::vec::Vec, + data: std::vec::Vec, +) -> crate::ExApiPlannedInstruction { + return crate::ExApiPlannedInstruction { + program_id: crate::MdProgramId( + kb_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID.to_string(), + ), + operation_code: operation_code.to_string(), + accounts, + data, + }; +} + +#[cfg(test)] +mod tests { + fn pubkey(value: &str) -> crate::MdPubkey { + return crate::MdPubkey(value.to_string()); + } + + fn policy(authority: &crate::MdPubkey) -> crate::ExApiExecutionPolicy { + return crate::ExApiExecutionPolicy { + cost_limit: crate::ExApiExecutionCostLimit { + max_spend_lamports: std::option::Option::Some(0), + max_fee_lamports: std::option::Option::Some(20_000), + max_compute_unit_price_micro_lamports: std::option::Option::None, + }, + authorized_signers: std::vec![authority.clone()], + dry_run: true, + post_execution_validation: crate::ExApiPostExecutionValidationPolicy { + canonical_insert_required: true, + core_extraction_required: true, + decode_replay_required: true, + materialization_required: true, + }, + ..crate::ExApiExecutionPolicy::default() + }; + } + + fn intent(operation: crate::ExMetadataSpmOperation) -> crate::ExMetadataSpmExecutionIntent { + let authority = match &operation { + crate::ExMetadataSpmOperation::Write { authority, .. } + | crate::ExMetadataSpmOperation::Initialize { authority, .. } + | crate::ExMetadataSpmOperation::SetAuthority { authority, .. } + | crate::ExMetadataSpmOperation::SetData { authority, .. } + | crate::ExMetadataSpmOperation::SetImmutable { authority, .. } + | crate::ExMetadataSpmOperation::Trim { authority, .. } + | crate::ExMetadataSpmOperation::Close { authority, .. } + | crate::ExMetadataSpmOperation::Allocate { authority, .. } + | crate::ExMetadataSpmOperation::Extend { authority, .. } => authority.clone(), + }; + return crate::ExMetadataSpmExecutionIntent { + intent_id: "spm-builder-test".to_string(), + fee_payer: authority.clone(), + policy: policy(&authority), + allow_destructive_operation: true, + operation, + }; + } + + #[test] + fn pda_helpers_match_the_exact_seed_contract() { + let program = pubkey(kb_program_ids::VOTE_PROGRAM_ID); + let authority = pubkey(kb_program_ids::SYSTEM_PROGRAM_ID); + let seed = vec![7_u8; crate::DC_METADATA_SPM_SEED_BYTES]; + let canonical = + crate::executor_metadata_solana_program_metadata_derive_canonical_pda(&program, &seed); + let noncanonical = + crate::executor_metadata_solana_program_metadata_derive_non_canonical_pda( + &program, &authority, &seed, + ); + assert!(canonical.is_ok()); + assert!(noncanonical.is_ok()); + let canonical = match canonical { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let noncanonical = match noncanonical { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + assert_ne!(canonical, noncanonical); + } + + #[test] + fn all_nine_operations_build_exact_tags_and_account_counts() { + let authority = pubkey(kb_program_ids::SYSTEM_PROGRAM_ID); + let account = pubkey(kb_program_ids::VOTE_PROGRAM_ID); + let destination = pubkey(kb_program_ids::STAKE_PROGRAM_ID); + let program = pubkey(kb_program_ids::BPF_LOADER_UPGRADEABLE_PROGRAM_ID); + let seed = vec![1_u8; crate::DC_METADATA_SPM_SEED_BYTES]; + let metadata = crate::executor_metadata_solana_program_metadata_derive_non_canonical_pda( + &program, &authority, &seed, + ); + assert!(metadata.is_ok()); + let metadata = match metadata { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let operations = std::vec![ + crate::ExMetadataSpmOperation::Write { + buffer: account.clone(), + authority: authority.clone(), + offset: 3, + source: crate::ExMetadataSpmWriteSource::Inline { data: vec![9, 8] }, + }, + crate::ExMetadataSpmOperation::Initialize { + metadata, + authority: authority.clone(), + program, + program_data: std::option::Option::None, + canonical: false, + seed, + encoding: crate::ExMetadataSpmEncoding::Utf8, + compression: crate::ExMetadataSpmCompression::None, + format: crate::ExMetadataSpmFormat::Json, + data_source: crate::ExMetadataSpmDataSource::Direct, + data: std::option::Option::Some(crate::ExMetadataSpmDataInput::Direct { + bytes: vec![123, 125], + }), + allocate_account: true, + }, + crate::ExMetadataSpmOperation::SetAuthority { + account: account.clone(), + authority: authority.clone(), + new_authority: std::option::Option::Some(destination.clone()), + program_context: std::option::Option::None, + }, + crate::ExMetadataSpmOperation::SetData { + metadata: account.clone(), + authority: authority.clone(), + encoding: crate::ExMetadataSpmEncoding::Utf8, + compression: crate::ExMetadataSpmCompression::None, + format: crate::ExMetadataSpmFormat::Json, + source: crate::ExMetadataSpmSetDataSource::PreserveExisting, + program_context: std::option::Option::None, + }, + crate::ExMetadataSpmOperation::SetImmutable { + metadata: account.clone(), + authority: authority.clone(), + program_context: std::option::Option::None, + }, + crate::ExMetadataSpmOperation::Trim { + account: account.clone(), + authority: authority.clone(), + destination: destination.clone(), + program_context: std::option::Option::None, + }, + crate::ExMetadataSpmOperation::Close { + account: account.clone(), + authority: authority.clone(), + destination, + program_context: std::option::Option::None, + }, + crate::ExMetadataSpmOperation::Allocate { + buffer: authority.clone(), + authority: authority.clone(), + seed: std::option::Option::None, + program_context: std::option::Option::None, + canonical: false, + allocate_account: false, + }, + crate::ExMetadataSpmOperation::Extend { + account, + authority, + length: 64, + program_context: std::option::Option::None, + }, + ]; + let expected = + [(0_u8, 3_usize), (1, 5), (2, 4), (3, 5), (4, 4), (5, 6), (6, 5), (7, 5), (8, 4)]; + for (position, operation) in operations.into_iter().enumerate() { + let plan = crate::executor_metadata_solana_program_metadata_build_prepared_plan( + &intent(operation), + ); + assert!(plan.is_ok()); + let plan = match plan { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => continue, + }; + assert_eq!(plan.instructions.len(), 1); + assert_eq!(plan.instructions[0].data[0], expected[position].0); + assert_eq!(plan.instructions[0].accounts.len(), expected[position].1); + } + } + + #[test] + fn initialize_canonicity_is_explicit_without_program_data_inference() { + let authority = pubkey(kb_program_ids::SYSTEM_PROGRAM_ID); + let program = pubkey(kb_program_ids::VOTE_PROGRAM_ID); + let seed = vec![2_u8; crate::DC_METADATA_SPM_SEED_BYTES]; + let metadata = + crate::executor_metadata_solana_program_metadata_derive_canonical_pda(&program, &seed); + assert!(metadata.is_ok()); + let metadata = match metadata { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let value = intent(crate::ExMetadataSpmOperation::Initialize { + metadata, + authority, + program, + program_data: std::option::Option::None, + canonical: true, + seed, + encoding: crate::ExMetadataSpmEncoding::Utf8, + compression: crate::ExMetadataSpmCompression::None, + format: crate::ExMetadataSpmFormat::Json, + data_source: crate::ExMetadataSpmDataSource::Direct, + data: std::option::Option::Some(crate::ExMetadataSpmDataInput::Direct { + bytes: vec![123, 125], + }), + allocate_account: true, + }); + assert!( + crate::executor_metadata_solana_program_metadata_build_prepared_plan(&value).is_ok() + ); + } + + #[test] + fn inline_content_is_bounded_and_large_content_requires_a_buffer() { + let authority = pubkey(kb_program_ids::SYSTEM_PROGRAM_ID); + let value = intent(crate::ExMetadataSpmOperation::Write { + buffer: pubkey(kb_program_ids::VOTE_PROGRAM_ID), + authority, + offset: 0, + source: crate::ExMetadataSpmWriteSource::Inline { + data: vec![0; crate::EX_METADATA_SPM_MAX_INLINE_DATA_BYTES + 1], + }, + }); + let result = crate::executor_metadata_solana_program_metadata_build_prepared_plan(&value); + assert_eq!( + result.as_ref().err().map(kb_core::Error::code), + std::option::Option::Some("execution_metadata_spm_inline_data_too_large") + ); + } + + #[test] + fn allocate_enforces_keypair_identity_and_exact_pda_derivation() { + let authority = pubkey(kb_program_ids::SYSTEM_PROGRAM_ID); + let invalid_keypair = intent(crate::ExMetadataSpmOperation::Allocate { + buffer: pubkey(kb_program_ids::VOTE_PROGRAM_ID), + authority: authority.clone(), + seed: std::option::Option::None, + program_context: std::option::Option::None, + canonical: false, + allocate_account: false, + }); + let invalid_keypair_result = + crate::executor_metadata_solana_program_metadata_build_prepared_plan(&invalid_keypair); + assert_eq!( + invalid_keypair_result.as_ref().err().map(kb_core::Error::code), + std::option::Option::Some("execution_metadata_spm_allocate_keypair_authority_mismatch") + ); + let seed = vec![4_u8; crate::DC_METADATA_SPM_SEED_BYTES]; + let context = crate::ExMetadataSpmProgramContext { + program: pubkey(kb_program_ids::VOTE_PROGRAM_ID), + program_data: std::option::Option::None, + }; + let expected = crate::executor_metadata_solana_program_metadata_derive_non_canonical_pda( + &context.program, + &authority, + &seed, + ); + assert!(expected.is_ok()); + let expected = match expected { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let valid_pda = intent(crate::ExMetadataSpmOperation::Allocate { + buffer: expected, + authority, + seed: std::option::Option::Some(seed), + program_context: std::option::Option::Some(context), + canonical: false, + allocate_account: true, + }); + assert!( + crate::executor_metadata_solana_program_metadata_build_prepared_plan(&valid_pda) + .is_ok() + ); + } + + #[test] + fn destructive_operations_require_approval_and_common_safety() { + let authority = pubkey(kb_program_ids::SYSTEM_PROGRAM_ID); + let mut value = intent(crate::ExMetadataSpmOperation::Close { + account: pubkey(kb_program_ids::VOTE_PROGRAM_ID), + authority: authority.clone(), + destination: pubkey(kb_program_ids::STAKE_PROGRAM_ID), + program_context: std::option::Option::None, + }); + value.allow_destructive_operation = false; + let denied = crate::executor_metadata_solana_program_metadata_build_prepared_plan(&value); + assert_eq!( + denied.as_ref().err().map(kb_core::Error::code), + std::option::Option::Some("execution_metadata_spm_destructive_approval_required") + ); + value.allow_destructive_operation = true; + value.policy.authorized_signers.clear(); + let denied_plan = + crate::executor_metadata_solana_program_metadata_build_prepared_plan(&value); + assert_eq!( + denied_plan.as_ref().err().map(kb_core::Error::code), + std::option::Option::Some("execution_metadata_spm_safety_denied") + ); + } +} diff --git a/kb-lib/src/executor/metadata/solana_program_metadata/constants.rs b/kb-lib/src/executor/metadata/solana_program_metadata/constants.rs new file mode 100644 index 0000000..fc9dc05 --- /dev/null +++ b/kb-lib/src/executor/metadata/solana_program_metadata/constants.rs @@ -0,0 +1,55 @@ +// file: kb-lib/src/executor/metadata/solana_program_metadata/constants.rs +// version: 1 + +//! Stable Solana Program Metadata executor constants. + +/// Stable executor name. +pub(crate) const EX_METADATA_SPM_EXECUTOR_NAME: &str = + "kb-lib.executor.metadata.solana_program_metadata"; +/// Conservative maximum inline content accepted by one instruction builder. +/// +/// Larger payloads must use a pre-funded Buffer account so the final transaction remains within +/// the Solana packet boundary after signatures, account keys and message framing are included. +pub const EX_METADATA_SPM_MAX_INLINE_DATA_BYTES: usize = 1_024; +/// Stable operation code for `Write`. +pub const EX_METADATA_SPM_WRITE_OPERATION: &str = "metadata.solana_program_metadata.write"; +/// Stable operation code for `Initialize`. +pub const EX_METADATA_SPM_INITIALIZE_OPERATION: &str = + "metadata.solana_program_metadata.initialize"; +/// Stable operation code for `SetAuthority`. +pub const EX_METADATA_SPM_SET_AUTHORITY_OPERATION: &str = + "metadata.solana_program_metadata.set_authority"; +/// Stable operation code for `SetData`. +pub const EX_METADATA_SPM_SET_DATA_OPERATION: &str = "metadata.solana_program_metadata.set_data"; +/// Stable operation code for `SetImmutable`. +pub const EX_METADATA_SPM_SET_IMMUTABLE_OPERATION: &str = + "metadata.solana_program_metadata.set_immutable"; +/// Stable operation code for `Trim`. +pub const EX_METADATA_SPM_TRIM_OPERATION: &str = "metadata.solana_program_metadata.trim"; +/// Stable operation code for `Close`. +pub const EX_METADATA_SPM_CLOSE_OPERATION: &str = "metadata.solana_program_metadata.close"; +/// Stable operation code for `Allocate`. +pub const EX_METADATA_SPM_ALLOCATE_OPERATION: &str = "metadata.solana_program_metadata.allocate"; +/// Stable operation code for `Extend`. +pub const EX_METADATA_SPM_EXTEND_OPERATION: &str = "metadata.solana_program_metadata.extend"; +/// Exact current operation inventory implemented by the executor. +pub const EX_METADATA_SPM_SUPPORTED_OPERATION_CODES: &[&str] = &[ + crate::EX_METADATA_SPM_WRITE_OPERATION, + crate::EX_METADATA_SPM_INITIALIZE_OPERATION, + crate::EX_METADATA_SPM_SET_AUTHORITY_OPERATION, + crate::EX_METADATA_SPM_SET_DATA_OPERATION, + crate::EX_METADATA_SPM_SET_IMMUTABLE_OPERATION, + crate::EX_METADATA_SPM_TRIM_OPERATION, + crate::EX_METADATA_SPM_CLOSE_OPERATION, + crate::EX_METADATA_SPM_ALLOCATE_OPERATION, + crate::EX_METADATA_SPM_EXTEND_OPERATION, +]; +/// Operations that require explicit operator approval before plan construction. +pub const EX_METADATA_SPM_DESTRUCTIVE_OPERATION_CODES: &[&str] = &[ + crate::EX_METADATA_SPM_WRITE_OPERATION, + crate::EX_METADATA_SPM_SET_AUTHORITY_OPERATION, + crate::EX_METADATA_SPM_SET_DATA_OPERATION, + crate::EX_METADATA_SPM_SET_IMMUTABLE_OPERATION, + crate::EX_METADATA_SPM_TRIM_OPERATION, + crate::EX_METADATA_SPM_CLOSE_OPERATION, +]; diff --git a/kb-lib/src/executor/metadata/solana_program_metadata/executor.rs b/kb-lib/src/executor/metadata/solana_program_metadata/executor.rs new file mode 100644 index 0000000..3862d8d --- /dev/null +++ b/kb-lib/src/executor/metadata/solana_program_metadata/executor.rs @@ -0,0 +1,224 @@ +// file: kb-lib/src/executor/metadata/solana_program_metadata/executor.rs +// version: 1 + +//! Exact capability dispatch for Solana Program Metadata. + +/// Typed Solana Program Metadata executor. +#[derive(Clone, Debug, Default)] +pub struct ExMetadataSolanaProgramMetadataExecutor; + +impl crate::ExMetadataSolanaProgramMetadataExecutor { + fn exact_capability( + &self, + program_id: &crate::MdProgramId, + operation_code: &str, + ) -> crate::ExApiExecutionCapability { + if program_id.0 != kb_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID { + return crate::ExApiExecutionCapability::unsupported( + "execution_metadata_spm_program_not_owned", + format!("program {} is not Solana Program Metadata", program_id.0), + ); + } + if crate::EX_METADATA_SPM_SUPPORTED_OPERATION_CODES + .iter() + .any(|candidate| return *candidate == operation_code) + { + return crate::ExApiExecutionCapability::supported(operation_code); + } + return crate::ExApiExecutionCapability::unsupported( + "execution_metadata_spm_operation_unsupported", + format!("Solana Program Metadata operation {operation_code} is not implemented"), + ); + } +} + +impl crate::ExApiTypedInstructionExecutor for crate::ExMetadataSolanaProgramMetadataExecutor { + type Intent = crate::ExMetadataSpmExecutionIntent; + + fn capability( + &self, + program_id: &crate::MdProgramId, + operation_code: &str, + ) -> crate::ExApiExecutionCapability { + return self.exact_capability(program_id, operation_code); + } + + fn build_prepared_plan( + &self, + intent: &Self::Intent, + ) -> kb_core::Result { + let program_id = crate::MdProgramId( + kb_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID.to_string(), + ); + return match self.exact_capability(&program_id, intent.operation.operation_code()) { + crate::ExApiExecutionCapability::Supported { operation_code: _ } => { + crate::executor_metadata_solana_program_metadata_build_prepared_plan(intent) + }, + crate::ExApiExecutionCapability::Unsupported { reason_code, reason } => { + std::result::Result::Err(kb_core::Error::new(reason_code, reason)) + }, + }; + } +} + +impl crate::ExApiInstructionExecutor for crate::ExMetadataSolanaProgramMetadataExecutor { + fn executor_name(&self) -> &'static str { + return crate::EX_METADATA_SPM_EXECUTOR_NAME; + } + + fn executor_version(&self) -> &'static str { + return env!("CARGO_PKG_VERSION"); + } + + fn program_ids(&self) -> &'static [&'static str] { + return &[kb_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID]; + } + + fn supports_request( + &self, + request: &crate::ExApiExecutionRequest, + ) -> crate::ExApiExecutionSupport { + return if self + .exact_capability(&request.program_id, &request.operation_code) + .is_supported() + { + crate::ExApiExecutionSupport::Yes + } else { + crate::ExApiExecutionSupport::No + }; + } + + fn build_plan( + &self, + request: &crate::ExApiExecutionRequest, + ) -> kb_core::Result { + match self.exact_capability(&request.program_id, &request.operation_code) { + crate::ExApiExecutionCapability::Supported { operation_code: _ } => {}, + crate::ExApiExecutionCapability::Unsupported { reason_code, reason } => { + return std::result::Result::Err(kb_core::Error::new(reason_code, reason)); + }, + } + let intent = match serde_json::from_str::( + &request.payload_json, + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return std::result::Result::Err(kb_core::Error::new( + "execution_metadata_spm_intent_deserialize_failed", + error.to_string(), + )); + }, + }; + if intent.operation.operation_code() != request.operation_code { + return std::result::Result::Err(kb_core::Error::new( + "execution_operation_code_mismatch", + "request and typed Solana Program Metadata intent operations differ", + )); + } + let prepared = + match crate::ExApiTypedInstructionExecutor::build_prepared_plan(self, &intent) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let payload = match serde_json::to_value(&prepared) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return std::result::Result::Err(kb_core::Error::new( + "execution_metadata_spm_plan_serialize_failed", + error.to_string(), + )); + }, + }; + let payload_json = match crate::executor_api_serialize_payload_json(&payload) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return std::result::Result::Ok(crate::ExApiExecutionPlan { + executor_name: crate::EX_METADATA_SPM_EXECUTOR_NAME.to_string(), + instruction_count: prepared.instructions.len(), + payload_json, + }); + } +} + +#[cfg(test)] +mod tests { + #[test] + fn executor_matrix_matches_the_compiled_capability_inventory() { + let parsed = serde_json::from_str::(include_str!( + "../../../../../test-fixtures/contract-matrices/SOLANA_PROGRAM_METADATA_EXECUTOR_MATRIX.json" + )); + assert!(parsed.is_ok()); + let matrix = match parsed { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let operations = match matrix.get("operations").and_then(serde_json::Value::as_array) { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + assert_eq!( + matrix.get("maxInlineContentBytes").and_then(serde_json::Value::as_u64), + std::option::Option::Some(crate::EX_METADATA_SPM_MAX_INLINE_DATA_BYTES as u64) + ); + assert_eq!( + matrix.get("canonicitySelection").and_then(serde_json::Value::as_str), + std::option::Option::Some( + "explicit_intent_flag_not_inferred_from_program_data_presence" + ) + ); + assert_eq!(operations.len(), crate::EX_METADATA_SPM_SUPPORTED_OPERATION_CODES.len()); + for (position, row) in operations.iter().enumerate() { + assert_eq!( + row.get("tag").and_then(serde_json::Value::as_u64), + std::option::Option::Some(position as u64) + ); + assert_eq!( + row.get("operationCode").and_then(serde_json::Value::as_str), + std::option::Option::Some( + crate::EX_METADATA_SPM_SUPPORTED_OPERATION_CODES[position] + ) + ); + assert_eq!( + row.get("requiresExplicitApproval").and_then(serde_json::Value::as_bool), + std::option::Option::Some( + crate::EX_METADATA_SPM_DESTRUCTIVE_OPERATION_CODES + .contains(&crate::EX_METADATA_SPM_SUPPORTED_OPERATION_CODES[position]) + ) + ); + assert_eq!( + row.get("deprecated").and_then(serde_json::Value::as_bool), + std::option::Option::Some(false) + ); + assert_eq!( + row.get("replaced").and_then(serde_json::Value::as_bool), + std::option::Option::Some(false) + ); + } + } + + #[test] + fn capabilities_cover_exactly_the_nine_stable_operations() { + let executor = crate::ExMetadataSolanaProgramMetadataExecutor; + let program_id = crate::MdProgramId( + kb_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID.to_string(), + ); + for operation in crate::EX_METADATA_SPM_SUPPORTED_OPERATION_CODES { + let capability = + crate::ExApiTypedInstructionExecutor::capability(&executor, &program_id, operation); + assert!(capability.is_supported()); + } + let unknown = crate::ExApiTypedInstructionExecutor::capability( + &executor, + &program_id, + "metadata.solana_program_metadata.unknown", + ); + assert!(!unknown.is_supported()); + let foreign = crate::ExApiTypedInstructionExecutor::capability( + &executor, + &crate::MdProgramId(kb_program_ids::SYSTEM_PROGRAM_ID.to_string()), + crate::EX_METADATA_SPM_WRITE_OPERATION, + ); + assert!(!foreign.is_supported()); + } +} diff --git a/kb-lib/src/executor/metadata/solana_program_metadata/intent.rs b/kb-lib/src/executor/metadata/solana_program_metadata/intent.rs new file mode 100644 index 0000000..c2a1c80 --- /dev/null +++ b/kb-lib/src/executor/metadata/solana_program_metadata/intent.rs @@ -0,0 +1,471 @@ +// file: kb-lib/src/executor/metadata/solana_program_metadata/intent.rs +// version: 1 + +//! Typed intents for the Solana Program Metadata executor. + +use ts_rs::TS; // rust-rules: derive-import + +/// Encoding written to a Solana Program Metadata account. +#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, TS)] +#[repr(u8)] +#[serde(rename_all = "snake_case")] +#[ts( + export, + export_to = "../frontend/ts/bindings/kb_lib/executor/metadata/solana_program_metadata/intent/ExMetadataSpmEncoding.ts" +)] +pub enum ExMetadataSpmEncoding { + /// No encoding declaration. + None = 0, + /// UTF-8 encoding. + Utf8 = 1, + /// Base58 encoding. + Base58 = 2, + /// Base64 encoding. + Base64 = 3, +} + +impl crate::ExMetadataSpmEncoding { + /// Returns the exact wire value. + pub const fn wire_value(self) -> u8 { + return self as u8; + } +} + +/// Compression written to a Solana Program Metadata account. +#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, TS)] +#[repr(u8)] +#[serde(rename_all = "snake_case")] +#[ts( + export, + export_to = "../frontend/ts/bindings/kb_lib/executor/metadata/solana_program_metadata/intent/ExMetadataSpmCompression.ts" +)] +pub enum ExMetadataSpmCompression { + /// No compression declaration. + None = 0, + /// Gzip compression. + Gzip = 1, + /// Zlib compression. + Zlib = 2, +} + +impl crate::ExMetadataSpmCompression { + /// Returns the exact wire value. + pub const fn wire_value(self) -> u8 { + return self as u8; + } +} + +/// Structured format written to a Solana Program Metadata account. +#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, TS)] +#[repr(u8)] +#[serde(rename_all = "snake_case")] +#[ts( + export, + export_to = "../frontend/ts/bindings/kb_lib/executor/metadata/solana_program_metadata/intent/ExMetadataSpmFormat.ts" +)] +pub enum ExMetadataSpmFormat { + /// No structured format declaration. + None = 0, + /// JSON format. + Json = 1, + /// YAML format. + Yaml = 2, + /// TOML format. + Toml = 3, +} + +impl crate::ExMetadataSpmFormat { + /// Returns the exact wire value. + pub const fn wire_value(self) -> u8 { + return self as u8; + } +} + +/// On-chain data source written to a Solana Program Metadata account. +#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, TS)] +#[repr(u8)] +#[serde(rename_all = "snake_case")] +#[ts( + export, + export_to = "../frontend/ts/bindings/kb_lib/executor/metadata/solana_program_metadata/intent/ExMetadataSpmDataSource.ts" +)] +pub enum ExMetadataSpmDataSource { + /// Direct on-chain bytes. + Direct = 0, + /// UTF-8 URL stored on-chain without fetching it. + Url = 1, + /// Fixed reference to another Solana account. + External = 2, +} + +impl crate::ExMetadataSpmDataSource { + /// Returns the exact wire value. + pub const fn wire_value(self) -> u8 { + return self as u8; + } +} + +/// Exact inline content accepted by `Initialize` and `SetData`. +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, TS)] +#[serde(tag = "source", rename_all = "snake_case")] +#[ts( + export, + export_to = "../frontend/ts/bindings/kb_lib/executor/metadata/solana_program_metadata/intent/ExMetadataSpmDataInput.ts" +)] +pub enum ExMetadataSpmDataInput { + /// Direct on-chain bytes. + Direct { + /// Exact bytes to store. + bytes: std::vec::Vec, + }, + /// UTF-8 URL retained on-chain without remote resolution. + Url { + /// URL text to store. + url: std::string::String, + }, + /// Fixed reference to another account's data. + External { + /// Referenced account. + address: crate::MdPubkey, + /// Byte offset inside the referenced account. + offset: u32, + /// Optional non-zero length; `None` means the remainder of the account. + length: std::option::Option, + }, +} + +impl crate::ExMetadataSpmDataInput { + /// Returns the exact data-source discriminator. + pub const fn data_source(&self) -> crate::ExMetadataSpmDataSource { + return match self { + Self::Direct { bytes: _ } => crate::ExMetadataSpmDataSource::Direct, + Self::Url { url: _ } => crate::ExMetadataSpmDataSource::Url, + Self::External { address: _, offset: _, length: _ } => { + crate::ExMetadataSpmDataSource::External + }, + }; + } +} + +/// Source used by the `Write` instruction. +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, TS)] +#[serde(tag = "source", rename_all = "snake_case")] +#[ts( + export, + export_to = "../frontend/ts/bindings/kb_lib/executor/metadata/solana_program_metadata/intent/ExMetadataSpmWriteSource.ts" +)] +pub enum ExMetadataSpmWriteSource { + /// Inline bytes placed after the offset argument. + Inline { + /// Exact bytes to write. + data: std::vec::Vec, + }, + /// Copy all data from another Buffer account. + Buffer { + /// Source Buffer account. + source_buffer: crate::MdPubkey, + }, +} + +/// Source used by the `SetData` instruction. +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, TS)] +#[serde(tag = "source", rename_all = "snake_case")] +#[ts( + export, + export_to = "../frontend/ts/bindings/kb_lib/executor/metadata/solana_program_metadata/intent/ExMetadataSpmSetDataSource.ts" +)] +pub enum ExMetadataSpmSetDataSource { + /// Runtime-supported three-byte variant that preserves current data bytes. + PreserveExisting, + /// Replace data with inline content. + Inline { + /// Exact inline content. + data: crate::ExMetadataSpmDataInput, + }, + /// Copy data from one Buffer account. + Buffer { + /// Source Buffer account. + buffer: crate::MdPubkey, + /// Source discriminator that the resulting metadata account must declare. + data_source: crate::ExMetadataSpmDataSource, + }, +} + +/// Optional program-upgrade-authority validation context. +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, TS)] +#[ts( + export, + export_to = "../frontend/ts/bindings/kb_lib/executor/metadata/solana_program_metadata/intent/ExMetadataSpmProgramContext.ts" +)] +pub struct ExMetadataSpmProgramContext { + /// Executable program account. + pub program: crate::MdPubkey, + /// Optional upgradeable-loader ProgramData account used when the runtime must validate it. + pub program_data: std::option::Option, +} + +/// One exact Solana Program Metadata operation. +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, TS)] +#[serde(tag = "operation", rename_all = "snake_case")] +#[ts( + export, + export_to = "../frontend/ts/bindings/kb_lib/executor/metadata/solana_program_metadata/intent/ExMetadataSpmOperation.ts" +)] +pub enum ExMetadataSpmOperation { + /// Write bytes to a Buffer account. + Write { + /// Buffer account to mutate. + buffer: crate::MdPubkey, + /// Current Buffer authority. + authority: crate::MdPubkey, + /// Byte offset inside the Buffer payload. + offset: u32, + /// Inline or Buffer source. + source: crate::ExMetadataSpmWriteSource, + }, + /// Initialize one canonical or non-canonical Metadata PDA. + Initialize { + /// Metadata PDA to initialize. + metadata: crate::MdPubkey, + /// Authority signing the initialization. + authority: crate::MdPubkey, + /// Program described by this metadata account. + program: crate::MdPubkey, + /// Optional ProgramData account used for an upgradeable program. + program_data: std::option::Option, + /// Whether the Metadata PDA uses the canonical `[program, seed]` derivation. + canonical: bool, + /// Exact fixed-size seed bytes. + seed: std::vec::Vec, + /// Content encoding. + encoding: crate::ExMetadataSpmEncoding, + /// Content compression. + compression: crate::ExMetadataSpmCompression, + /// Structured content format. + format: crate::ExMetadataSpmFormat, + /// Declared data source when using a preallocated Buffer. + data_source: crate::ExMetadataSpmDataSource, + /// Optional inline content; absent means the Metadata account is a preallocated Buffer. + data: std::option::Option, + /// Whether the instruction may invoke the System Program to allocate the PDA. + allocate_account: bool, + }, + /// Change or remove one Buffer or Metadata authority. + SetAuthority { + /// Buffer or Metadata account. + account: crate::MdPubkey, + /// Current authority. + authority: crate::MdPubkey, + /// New authority, or `None` to remove a Metadata authority. + new_authority: std::option::Option, + /// Optional upgrade-authority validation context. + program_context: std::option::Option, + }, + /// Change Metadata content descriptors and data. + SetData { + /// Metadata account. + metadata: crate::MdPubkey, + /// Current authority. + authority: crate::MdPubkey, + /// Content encoding. + encoding: crate::ExMetadataSpmEncoding, + /// Content compression. + compression: crate::ExMetadataSpmCompression, + /// Structured content format. + format: crate::ExMetadataSpmFormat, + /// Inline, Buffer or preserve-existing source. + source: crate::ExMetadataSpmSetDataSource, + /// Optional upgrade-authority validation context. + program_context: std::option::Option, + }, + /// Permanently make one Metadata account immutable. + SetImmutable { + /// Metadata account. + metadata: crate::MdPubkey, + /// Current authority. + authority: crate::MdPubkey, + /// Optional upgrade-authority validation context. + program_context: std::option::Option, + }, + /// Resize one Buffer or Metadata account to its logical minimum and refund excess lamports. + Trim { + /// Buffer or Metadata account. + account: crate::MdPubkey, + /// Current authority. + authority: crate::MdPubkey, + /// Lamport destination. + destination: crate::MdPubkey, + /// Optional upgrade-authority validation context. + program_context: std::option::Option, + }, + /// Close one mutable Buffer or Metadata account. + Close { + /// Buffer or Metadata account. + account: crate::MdPubkey, + /// Current authority. + authority: crate::MdPubkey, + /// Lamport destination. + destination: crate::MdPubkey, + /// Optional upgrade-authority validation context. + program_context: std::option::Option, + }, + /// Allocate one pre-funded Buffer account. + Allocate { + /// Buffer account. + buffer: crate::MdPubkey, + /// Authority signing the allocation. + authority: crate::MdPubkey, + /// Optional seed for a PDA Buffer; absent selects a pre-created keypair account. + seed: std::option::Option>, + /// Optional upgrade-authority validation context. + program_context: std::option::Option, + /// Whether a PDA Buffer uses the canonical `[program, seed]` derivation. + canonical: bool, + /// Whether the instruction may invoke the System Program to allocate the account. + allocate_account: bool, + }, + /// Extend one pre-funded Buffer or Metadata account. + Extend { + /// Buffer or Metadata account. + account: crate::MdPubkey, + /// Current authority. + authority: crate::MdPubkey, + /// Number of bytes to append to account capacity. + length: u16, + /// Optional upgrade-authority validation context. + program_context: std::option::Option, + }, +} + +impl crate::ExMetadataSpmOperation { + /// Returns the stable operation code. + pub const fn operation_code(&self) -> &'static str { + return match self { + Self::Write { + buffer: _, + authority: _, + offset: _, + source: _, + } => crate::EX_METADATA_SPM_WRITE_OPERATION, + Self::Initialize { + metadata: _, + authority: _, + program: _, + program_data: _, + canonical: _, + seed: _, + encoding: _, + compression: _, + format: _, + data_source: _, + data: _, + allocate_account: _, + } => crate::EX_METADATA_SPM_INITIALIZE_OPERATION, + Self::SetAuthority { + account: _, + authority: _, + new_authority: _, + program_context: _, + } => crate::EX_METADATA_SPM_SET_AUTHORITY_OPERATION, + Self::SetData { + metadata: _, + authority: _, + encoding: _, + compression: _, + format: _, + source: _, + program_context: _, + } => crate::EX_METADATA_SPM_SET_DATA_OPERATION, + Self::SetImmutable { + metadata: _, + authority: _, + program_context: _, + } => crate::EX_METADATA_SPM_SET_IMMUTABLE_OPERATION, + Self::Trim { + account: _, + authority: _, + destination: _, + program_context: _, + } => crate::EX_METADATA_SPM_TRIM_OPERATION, + Self::Close { + account: _, + authority: _, + destination: _, + program_context: _, + } => crate::EX_METADATA_SPM_CLOSE_OPERATION, + Self::Allocate { + buffer: _, + authority: _, + seed: _, + program_context: _, + canonical: _, + allocate_account: _, + } => crate::EX_METADATA_SPM_ALLOCATE_OPERATION, + Self::Extend { + account: _, + authority: _, + length: _, + program_context: _, + } => crate::EX_METADATA_SPM_EXTEND_OPERATION, + }; + } + + /// Returns whether the operation can overwrite, destroy or irreversibly constrain state. + pub const fn requires_explicit_approval(&self) -> bool { + return matches!( + self, + Self::Write { + buffer: _, + authority: _, + offset: _, + source: _ + } | Self::SetAuthority { + account: _, + authority: _, + new_authority: _, + program_context: _, + } | Self::SetData { + metadata: _, + authority: _, + encoding: _, + compression: _, + format: _, + source: _, + program_context: _, + } | Self::SetImmutable { + metadata: _, + authority: _, + program_context: _ + } | Self::Trim { + account: _, + authority: _, + destination: _, + program_context: _ + } | Self::Close { + account: _, + authority: _, + destination: _, + program_context: _ + } + ); + } +} + +/// Complete Solana Program Metadata execution intent. +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, TS)] +#[ts( + export, + export_to = "../frontend/ts/bindings/kb_lib/executor/metadata/solana_program_metadata/intent/ExMetadataSpmExecutionIntent.ts" +)] +pub struct ExMetadataSpmExecutionIntent { + /// Caller-provided correlation identifier. + pub intent_id: std::string::String, + /// Transaction fee payer. + pub fee_payer: crate::MdPubkey, + /// Conservative common execution policy. + pub policy: crate::ExApiExecutionPolicy, + /// Explicit approval for overwrite, destructive or irreversible operations. + pub allow_destructive_operation: bool, + /// Exact operation to build. + pub operation: crate::ExMetadataSpmOperation, +} diff --git a/kb-lib/src/lib.rs b/kb-lib/src/lib.rs index 6b26df3..62fc277 100644 --- a/kb-lib/src/lib.rs +++ b/kb-lib/src/lib.rs @@ -1,5 +1,5 @@ // file: kb-lib/src/lib.rs -// version: 33 +// version: 36 //! Consolidated decoder, executor, materializer and shared model library. #![warn(missing_docs)] @@ -401,6 +401,30 @@ pub use self::decoder::decoder_metadata_solana_program_metadata_decode_metadata_ pub use self::decoder::decoder_spl_elgamal_registry_parse_elgamal_registry_state; /// Parses one Token-2022 Mint, Account, or Multisig state. pub use self::decoder::decoder_spl_token_2022_parse_token_2022_state; +/// Stable Solana Program Metadata `Allocate` operation code. +pub use self::executor::EX_METADATA_SPM_ALLOCATE_OPERATION; +/// Stable Solana Program Metadata `Close` operation code. +pub use self::executor::EX_METADATA_SPM_CLOSE_OPERATION; +/// Solana Program Metadata operations requiring explicit destructive approval. +pub use self::executor::EX_METADATA_SPM_DESTRUCTIVE_OPERATION_CODES; +/// Stable Solana Program Metadata `Extend` operation code. +pub use self::executor::EX_METADATA_SPM_EXTEND_OPERATION; +/// Stable Solana Program Metadata `Initialize` operation code. +pub use self::executor::EX_METADATA_SPM_INITIALIZE_OPERATION; +/// Conservative maximum inline Solana Program Metadata content size. +pub use self::executor::EX_METADATA_SPM_MAX_INLINE_DATA_BYTES; +/// Stable Solana Program Metadata `SetAuthority` operation code. +pub use self::executor::EX_METADATA_SPM_SET_AUTHORITY_OPERATION; +/// Stable Solana Program Metadata `SetData` operation code. +pub use self::executor::EX_METADATA_SPM_SET_DATA_OPERATION; +/// Stable Solana Program Metadata `SetImmutable` operation code. +pub use self::executor::EX_METADATA_SPM_SET_IMMUTABLE_OPERATION; +/// Exact Solana Program Metadata operation inventory. +pub use self::executor::EX_METADATA_SPM_SUPPORTED_OPERATION_CODES; +/// Stable Solana Program Metadata `Trim` operation code. +pub use self::executor::EX_METADATA_SPM_TRIM_OPERATION; +/// Stable Solana Program Metadata `Write` operation code. +pub use self::executor::EX_METADATA_SPM_WRITE_OPERATION; /// Exposes `EX_METAPLEX_TOKEN_METADATA_APPROVE_USE_AUTHORITY_OPERATION`. pub use self::executor::EX_METAPLEX_TOKEN_METADATA_APPROVE_USE_AUTHORITY_OPERATION; /// Exposes `EX_METAPLEX_TOKEN_METADATA_BURN_OPERATION`. @@ -1135,10 +1159,30 @@ pub use self::executor::ExLendingMarginfiV2Executor; pub use self::executor::ExLockRaydiumLpExecutor; /// Exposes the Metaplex Token Metadata executor. pub use self::executor::ExMetadataMetaplexTokenMetadataExecutor; -/// Exposes the reserved `ExMetadataSolanaProgramMetadataExecutor` implementation. +/// Exposes the typed `ExMetadataSolanaProgramMetadataExecutor` implementation. pub use self::executor::ExMetadataSolanaProgramMetadataExecutor; /// Exposes the reserved `ExMetadataSplNameServiceExecutor` implementation. pub use self::executor::ExMetadataSplNameServiceExecutor; +/// Solana Program Metadata compression intent. +pub use self::executor::ExMetadataSpmCompression; +/// Solana Program Metadata inline data intent. +pub use self::executor::ExMetadataSpmDataInput; +/// Solana Program Metadata data-source intent. +pub use self::executor::ExMetadataSpmDataSource; +/// Solana Program Metadata encoding intent. +pub use self::executor::ExMetadataSpmEncoding; +/// Complete Solana Program Metadata execution intent. +pub use self::executor::ExMetadataSpmExecutionIntent; +/// Solana Program Metadata format intent. +pub use self::executor::ExMetadataSpmFormat; +/// One Solana Program Metadata operation. +pub use self::executor::ExMetadataSpmOperation; +/// Optional Solana Program Metadata program context. +pub use self::executor::ExMetadataSpmProgramContext; +/// Solana Program Metadata `SetData` source intent. +pub use self::executor::ExMetadataSpmSetDataSource; +/// Solana Program Metadata `Write` source intent. +pub use self::executor::ExMetadataSpmWriteSource; /// Exposes the complete Metaplex Token Metadata execution intent. pub use self::executor::ExMetaplexTokenMetadataExecutionIntent; /// Exposes one current Metaplex Token Metadata operation. @@ -1349,6 +1393,10 @@ pub use self::executor::ExWeightedSwapStabbleExecutor; pub use self::executor::executor_api_serialize_payload_json; /// Exposes the pretty JSON payload serializer. pub use self::executor::executor_api_serialize_payload_json_pretty; +/// Derives the canonical Solana Program Metadata PDA. +pub use self::executor::executor_metadata_solana_program_metadata_derive_canonical_pda; +/// Derives the non-canonical Solana Program Metadata PDA. +pub use self::executor::executor_metadata_solana_program_metadata_derive_non_canonical_pda; /// Builds an unsigned durable nonce transaction from a prepared execution plan and validated nonce state. pub use self::executor::executor_solana_build_durable_nonce_transaction; /// Builds an unsigned legacy Solana transaction from a prepared execution plan. @@ -1391,7 +1439,7 @@ pub use self::materializer::MtLifecycleMaterializer; pub use self::materializer::MtLiquidityMaterializer; /// Exposes the legacy common business materializer trait. pub use self::materializer::MtMaterializer; -/// Token-2022, Metaplex and Solana Program Metadata materializer. +/// Compatibility wrapper for the Solana Program Metadata materializer. pub use self::materializer::MtMetadataMaterializer; /// Reserved NFT materializer. pub use self::materializer::MtNftMaterializer; @@ -1409,6 +1457,8 @@ pub use self::materializer::MtRewardsMaterializer; pub use self::materializer::MtRiskMaterializer; /// Reserved routing materializer. pub use self::materializer::MtRoutingMaterializer; +/// Stable Solana Program Metadata instruction materializer. +pub use self::materializer::MtSolanaProgramMetadataMaterializer; /// Stable native stake and vote materializer. pub use self::materializer::MtStakingMaterializer; /// Stable Token, ATA and Token-2022 account materializer. @@ -1972,6 +2022,8 @@ pub(crate) use self::decoder::decoder_spl_token_decode; pub(crate) use self::decoder::decoder_spl_token_entry_for_tag; /// Returns the first byte of one retained classic SPL Token payload. pub(crate) use self::decoder::decoder_spl_token_payload_tag; +/// Stable Solana Program Metadata executor name. +pub(crate) use self::executor::EX_METADATA_SPM_EXECUTOR_NAME; /// Stable operation code for the nonce advance injected by durable transaction assembly. pub(crate) use self::executor::EX_SOLANA_TRANSACTION_DURABLE_NONCE_ADVANCE_OPERATION; /// Maximum over-the-wire size of a Solana transaction packet. @@ -2004,6 +2056,8 @@ pub(crate) use self::executor::TRACING_TARGET_EXECUTOR_SPL_MEMO; pub(crate) use self::executor::TRACING_TARGET_EXECUTOR_SPL_TOKEN; /// Canonical SPL Token-2022 tracing target. pub(crate) use self::executor::TRACING_TARGET_EXECUTOR_SPL_TOKEN_2022; +/// Internal Solana Program Metadata prepared-plan builder. +pub(crate) use self::executor::executor_metadata_solana_program_metadata_build_prepared_plan; /// Internal Metaplex Token Metadata prepared-plan builder. pub(crate) use self::executor::executor_metaplex_token_metadata_build_prepared_plan; /// Crate-root access to `build_prepared_plan` from `address_lookup_table`. @@ -2052,41 +2106,315 @@ pub(crate) use self::executor::executor_spl_memo_build_prepared_plan; pub(crate) use self::executor::executor_spl_token_2022_build_prepared_plan; /// Crate-root access to `executor_spl_token_build_prepared_plan`. pub(crate) use self::executor::executor_spl_token_build_prepared_plan; -/// Canonical processor name for the native and SPL administration materializer. +/// Crate-root access to `MT_ADMIN_ACCEPTED_FAMILIES`. +pub(crate) use self::materializer::MT_ADMIN_ACCEPTED_FAMILIES; +/// Crate-root access to `MT_ADMIN_BPF_LOADER_UPGRADEABLE_ENTRIES`. +pub(crate) use self::materializer::MT_ADMIN_BPF_LOADER_UPGRADEABLE_ENTRIES; +/// Crate-root access to `MT_ADMIN_BPF_LOADER_UPGRADEABLE_SURFACE`. +pub(crate) use self::materializer::MT_ADMIN_BPF_LOADER_UPGRADEABLE_SURFACE; +/// Crate-root access to `MT_ADMIN_COMPONENT_NAME`. +pub(crate) use self::materializer::MT_ADMIN_COMPONENT_NAME; +/// Crate-root access to `MT_ADMIN_CONFIG_ENTRIES`. +pub(crate) use self::materializer::MT_ADMIN_CONFIG_ENTRIES; +/// Crate-root access to `MT_ADMIN_CONFIG_SURFACE`. +pub(crate) use self::materializer::MT_ADMIN_CONFIG_SURFACE; +/// Crate-root access to `MT_ADMIN_LOADER_V4_ENTRIES`. +pub(crate) use self::materializer::MT_ADMIN_LOADER_V4_ENTRIES; +/// Crate-root access to `MT_ADMIN_LOADER_V4_SURFACE`. +pub(crate) use self::materializer::MT_ADMIN_LOADER_V4_SURFACE; +/// Crate-root access to `MT_ADMIN_PROCESSOR_NAME`. pub(crate) use self::materializer::MT_ADMIN_PROCESSOR_NAME; -/// Event families accepted by the Solana Program Metadata instruction materializer. -pub(crate) use self::materializer::MT_METADATA_SPM_ACCEPTED_FAMILIES; -/// Stable processor name for Solana Program Metadata projections. -pub(crate) use self::materializer::MT_METADATA_SPM_PROCESSOR_NAME; -/// Stable projection schema version for Solana Program Metadata outputs. -pub(crate) use self::materializer::MT_METADATA_SPM_PROJECTION_VERSION; -/// Stable processor name for SPL Token and ATA risk facts. +/// Crate-root access to `MT_ADMIN_PROJECTION_VERSION`. +pub(crate) use self::materializer::MT_ADMIN_PROJECTION_VERSION; +/// Crate-root access to `MT_ADMIN_SPL_ELGAMAL_REGISTRY_ENTRIES`. +pub(crate) use self::materializer::MT_ADMIN_SPL_ELGAMAL_REGISTRY_ENTRIES; +/// Crate-root access to `MT_ADMIN_SPL_ELGAMAL_REGISTRY_SURFACE`. +pub(crate) use self::materializer::MT_ADMIN_SPL_ELGAMAL_REGISTRY_SURFACE; +/// Crate-root access to `MT_ADMIN_SPL_TOKEN_2022_EXTENSION_ADMIN_ENTRIES`. +pub(crate) use self::materializer::MT_ADMIN_SPL_TOKEN_2022_EXTENSION_ADMIN_ENTRIES; +/// Crate-root access to `MT_ADMIN_SPL_TOKEN_2022_SURFACE`. +pub(crate) use self::materializer::MT_ADMIN_SPL_TOKEN_2022_SURFACE; +/// Crate-root access to `MT_ADMIN_SPL_TOKEN_AUTHORITY_ENTRIES`. +pub(crate) use self::materializer::MT_ADMIN_SPL_TOKEN_AUTHORITY_ENTRIES; +/// Crate-root access to `MT_ADMIN_SPL_TOKEN_MULTISIG_ENTRIES`. +pub(crate) use self::materializer::MT_ADMIN_SPL_TOKEN_MULTISIG_ENTRIES; +/// Crate-root access to `MT_ADMIN_SPL_TOKEN_SURFACE`. +pub(crate) use self::materializer::MT_ADMIN_SPL_TOKEN_SURFACE; +/// Crate-root access to `MT_ADMIN_SYSTEM_ENTRIES`. +pub(crate) use self::materializer::MT_ADMIN_SYSTEM_ENTRIES; +/// Crate-root access to `MT_ADMIN_SYSTEM_SURFACE`. +pub(crate) use self::materializer::MT_ADMIN_SYSTEM_SURFACE; +/// Crate-root access to `MT_BRIDGE_ACCEPTED_FAMILIES`. +pub(crate) use self::materializer::MT_BRIDGE_ACCEPTED_FAMILIES; +/// Crate-root access to `MT_BRIDGE_COMPONENT_NAME`. +pub(crate) use self::materializer::MT_BRIDGE_COMPONENT_NAME; +/// Crate-root access to `MT_BRIDGE_PROCESSOR_NAME`. +pub(crate) use self::materializer::MT_BRIDGE_PROCESSOR_NAME; +/// Crate-root access to `MT_COMPLIANCE_AUDIT_ACCEPTED_FAMILIES`. +pub(crate) use self::materializer::MT_COMPLIANCE_AUDIT_ACCEPTED_FAMILIES; +/// Crate-root access to `MT_COMPLIANCE_AUDIT_BPF_LOADER_DEPRECATED_SURFACE`. +pub(crate) use self::materializer::MT_COMPLIANCE_AUDIT_BPF_LOADER_DEPRECATED_SURFACE; +/// Crate-root access to `MT_COMPLIANCE_AUDIT_BPF_LOADER_SURFACE`. +pub(crate) use self::materializer::MT_COMPLIANCE_AUDIT_BPF_LOADER_SURFACE; +/// Crate-root access to `MT_COMPLIANCE_AUDIT_BPF_LOADER_UPGRADEABLE_SURFACE`. +pub(crate) use self::materializer::MT_COMPLIANCE_AUDIT_BPF_LOADER_UPGRADEABLE_SURFACE; +/// Crate-root access to `MT_COMPLIANCE_AUDIT_COMPONENT_NAME`. +pub(crate) use self::materializer::MT_COMPLIANCE_AUDIT_COMPONENT_NAME; +/// Crate-root access to `MT_COMPLIANCE_AUDIT_COMPUTE_BUDGET_ENTRIES`. +pub(crate) use self::materializer::MT_COMPLIANCE_AUDIT_COMPUTE_BUDGET_ENTRIES; +/// Crate-root access to `MT_COMPLIANCE_AUDIT_COMPUTE_BUDGET_SURFACE`. +pub(crate) use self::materializer::MT_COMPLIANCE_AUDIT_COMPUTE_BUDGET_SURFACE; +/// Crate-root access to `MT_COMPLIANCE_AUDIT_LOADER_V4_COPY_ENTRIES`. +pub(crate) use self::materializer::MT_COMPLIANCE_AUDIT_LOADER_V4_COPY_ENTRIES; +/// Crate-root access to `MT_COMPLIANCE_AUDIT_LOADER_V4_SURFACE`. +pub(crate) use self::materializer::MT_COMPLIANCE_AUDIT_LOADER_V4_SURFACE; +/// Crate-root access to `MT_COMPLIANCE_AUDIT_PROCESSOR_NAME`. +pub(crate) use self::materializer::MT_COMPLIANCE_AUDIT_PROCESSOR_NAME; +/// Crate-root access to `MT_COMPLIANCE_AUDIT_PROJECTION_VERSION`. +pub(crate) use self::materializer::MT_COMPLIANCE_AUDIT_PROJECTION_VERSION; +/// Crate-root access to `MT_COMPLIANCE_AUDIT_WRITE_ENTRIES`. +pub(crate) use self::materializer::MT_COMPLIANCE_AUDIT_WRITE_ENTRIES; +/// Crate-root access to `MT_FEES_ACCEPTED_FAMILIES`. +pub(crate) use self::materializer::MT_FEES_ACCEPTED_FAMILIES; +/// Crate-root access to `MT_FEES_COMPONENT_NAME`. +pub(crate) use self::materializer::MT_FEES_COMPONENT_NAME; +/// Crate-root access to `MT_FEES_PROCESSOR_NAME`. +pub(crate) use self::materializer::MT_FEES_PROCESSOR_NAME; +/// Crate-root access to `MT_FEES_PROJECTION_VERSION`. +pub(crate) use self::materializer::MT_FEES_PROJECTION_VERSION; +/// Crate-root access to `MT_FEES_SPL_TOKEN_2022_SURFACE`. +pub(crate) use self::materializer::MT_FEES_SPL_TOKEN_2022_SURFACE; +/// Crate-root access to `MT_FEES_TOKEN_2022_FEE_ENTRIES`. +pub(crate) use self::materializer::MT_FEES_TOKEN_2022_FEE_ENTRIES; +/// Crate-root access to `MT_GOVERNANCE_ACCEPTED_FAMILIES`. +pub(crate) use self::materializer::MT_GOVERNANCE_ACCEPTED_FAMILIES; +/// Crate-root access to `MT_GOVERNANCE_COMPONENT_NAME`. +pub(crate) use self::materializer::MT_GOVERNANCE_COMPONENT_NAME; +/// Crate-root access to `MT_GOVERNANCE_PROCESSOR_NAME`. +pub(crate) use self::materializer::MT_GOVERNANCE_PROCESSOR_NAME; +/// Crate-root access to `MT_LENDING_ACCEPTED_FAMILIES`. +pub(crate) use self::materializer::MT_LENDING_ACCEPTED_FAMILIES; +/// Crate-root access to `MT_LENDING_COMPONENT_NAME`. +pub(crate) use self::materializer::MT_LENDING_COMPONENT_NAME; +/// Crate-root access to `MT_LENDING_PROCESSOR_NAME`. +pub(crate) use self::materializer::MT_LENDING_PROCESSOR_NAME; +/// Crate-root access to `MT_LIFECYCLE_ACCEPTED_FAMILIES`. +pub(crate) use self::materializer::MT_LIFECYCLE_ACCEPTED_FAMILIES; +/// Crate-root access to `MT_LIFECYCLE_ADDRESS_LOOKUP_TABLE_ENTRIES`. +pub(crate) use self::materializer::MT_LIFECYCLE_ADDRESS_LOOKUP_TABLE_ENTRIES; +/// Crate-root access to `MT_LIFECYCLE_ADDRESS_LOOKUP_TABLE_SURFACE`. +pub(crate) use self::materializer::MT_LIFECYCLE_ADDRESS_LOOKUP_TABLE_SURFACE; +/// Crate-root access to `MT_LIFECYCLE_BPF_LOADER_DEPRECATED_SURFACE`. +pub(crate) use self::materializer::MT_LIFECYCLE_BPF_LOADER_DEPRECATED_SURFACE; +/// Crate-root access to `MT_LIFECYCLE_BPF_LOADER_SURFACE`. +pub(crate) use self::materializer::MT_LIFECYCLE_BPF_LOADER_SURFACE; +/// Crate-root access to `MT_LIFECYCLE_BPF_LOADER_UPGRADEABLE_SURFACE`. +pub(crate) use self::materializer::MT_LIFECYCLE_BPF_LOADER_UPGRADEABLE_SURFACE; +/// Crate-root access to `MT_LIFECYCLE_COMPONENT_NAME`. +pub(crate) use self::materializer::MT_LIFECYCLE_COMPONENT_NAME; +/// Crate-root access to `MT_LIFECYCLE_FEATURE_ENTRIES`. +pub(crate) use self::materializer::MT_LIFECYCLE_FEATURE_ENTRIES; +/// Crate-root access to `MT_LIFECYCLE_FEATURE_SURFACE`. +pub(crate) use self::materializer::MT_LIFECYCLE_FEATURE_SURFACE; +/// Crate-root access to `MT_LIFECYCLE_IMMUTABLE_LOADER_ENTRIES`. +pub(crate) use self::materializer::MT_LIFECYCLE_IMMUTABLE_LOADER_ENTRIES; +/// Crate-root access to `MT_LIFECYCLE_LOADER_V4_ENTRIES`. +pub(crate) use self::materializer::MT_LIFECYCLE_LOADER_V4_ENTRIES; +/// Crate-root access to `MT_LIFECYCLE_LOADER_V4_SURFACE`. +pub(crate) use self::materializer::MT_LIFECYCLE_LOADER_V4_SURFACE; +/// Crate-root access to `MT_LIFECYCLE_PROCESSOR_NAME`. +pub(crate) use self::materializer::MT_LIFECYCLE_PROCESSOR_NAME; +/// Crate-root access to `MT_LIFECYCLE_PROJECTION_VERSION`. +pub(crate) use self::materializer::MT_LIFECYCLE_PROJECTION_VERSION; +/// Crate-root access to `MT_LIFECYCLE_SLASHING_ENTRIES`. +pub(crate) use self::materializer::MT_LIFECYCLE_SLASHING_ENTRIES; +/// Crate-root access to `MT_LIFECYCLE_SLASHING_SURFACE`. +pub(crate) use self::materializer::MT_LIFECYCLE_SLASHING_SURFACE; +/// Crate-root access to `MT_LIFECYCLE_SYSTEM_ACCOUNT_ENTRIES`. +pub(crate) use self::materializer::MT_LIFECYCLE_SYSTEM_ACCOUNT_ENTRIES; +/// Crate-root access to `MT_LIFECYCLE_SYSTEM_NONCE_ENTRIES`. +pub(crate) use self::materializer::MT_LIFECYCLE_SYSTEM_NONCE_ENTRIES; +/// Crate-root access to `MT_LIFECYCLE_SYSTEM_SURFACE`. +pub(crate) use self::materializer::MT_LIFECYCLE_SYSTEM_SURFACE; +/// Crate-root access to `MT_LIFECYCLE_UPGRADEABLE_LOADER_ENTRIES`. +pub(crate) use self::materializer::MT_LIFECYCLE_UPGRADEABLE_LOADER_ENTRIES; +/// Crate-root access to `MT_LIFECYCLE_ZK_ELGAMAL_CLOSE_ENTRIES`. +pub(crate) use self::materializer::MT_LIFECYCLE_ZK_ELGAMAL_CLOSE_ENTRIES; +/// Crate-root access to `MT_LIFECYCLE_ZK_ELGAMAL_PROOF_SURFACE`. +pub(crate) use self::materializer::MT_LIFECYCLE_ZK_ELGAMAL_PROOF_SURFACE; +/// Crate-root access to `MT_LIFECYCLE_ZK_ELGAMAL_VERIFY_ENTRIES`. +pub(crate) use self::materializer::MT_LIFECYCLE_ZK_ELGAMAL_VERIFY_ENTRIES; +/// Crate-root access to `MT_LIQUIDITY_ACCEPTED_FAMILIES`. +pub(crate) use self::materializer::MT_LIQUIDITY_ACCEPTED_FAMILIES; +/// Crate-root access to `MT_LIQUIDITY_COMPONENT_NAME`. +pub(crate) use self::materializer::MT_LIQUIDITY_COMPONENT_NAME; +/// Crate-root access to `MT_LIQUIDITY_PROCESSOR_NAME`. +pub(crate) use self::materializer::MT_LIQUIDITY_PROCESSOR_NAME; +/// Crate-root access to `MT_METADATA_METAPLEX_TOKEN_METADATA_COMPONENT_NAME`. +pub(crate) use self::materializer::MT_METADATA_METAPLEX_TOKEN_METADATA_COMPONENT_NAME; +/// Crate-root access to `MT_METADATA_METAPLEX_TOKEN_METADATA_PROCESSOR_NAME`. +pub(crate) use self::materializer::MT_METADATA_METAPLEX_TOKEN_METADATA_PROCESSOR_NAME; +/// Crate-root access to `MT_METADATA_METAPLEX_TOKEN_METADATA_PROJECTION_VERSION`. +pub(crate) use self::materializer::MT_METADATA_METAPLEX_TOKEN_METADATA_PROJECTION_VERSION; +/// Crate-root access to `MT_METADATA_SOLANA_PROGRAM_METADATA_ACCEPTED_FAMILIES`. +pub(crate) use self::materializer::MT_METADATA_SOLANA_PROGRAM_METADATA_ACCEPTED_FAMILIES; +/// Crate-root access to `MT_METADATA_SOLANA_PROGRAM_METADATA_COMPONENT_NAME`. +pub(crate) use self::materializer::MT_METADATA_SOLANA_PROGRAM_METADATA_COMPONENT_NAME; +/// Crate-root access to `MT_METADATA_SOLANA_PROGRAM_METADATA_PROCESSOR_NAME`. +pub(crate) use self::materializer::MT_METADATA_SOLANA_PROGRAM_METADATA_PROCESSOR_NAME; +/// Crate-root access to `MT_METADATA_SOLANA_PROGRAM_METADATA_PROJECTION_VERSION`. +pub(crate) use self::materializer::MT_METADATA_SOLANA_PROGRAM_METADATA_PROJECTION_VERSION; +/// Crate-root access to `MT_METADATA_TOKEN_2022_COMPONENT_NAME`. +pub(crate) use self::materializer::MT_METADATA_TOKEN_2022_COMPONENT_NAME; +/// Crate-root access to `MT_METADATA_TOKEN_2022_PROCESSOR_NAME`. +pub(crate) use self::materializer::MT_METADATA_TOKEN_2022_PROCESSOR_NAME; +/// Crate-root access to `MT_METADATA_TOKEN_2022_PROJECTION_VERSION`. +pub(crate) use self::materializer::MT_METADATA_TOKEN_2022_PROJECTION_VERSION; +/// Crate-root access to `MT_NFT_ACCEPTED_FAMILIES`. +pub(crate) use self::materializer::MT_NFT_ACCEPTED_FAMILIES; +/// Crate-root access to `MT_NFT_COMPONENT_NAME`. +pub(crate) use self::materializer::MT_NFT_COMPONENT_NAME; +/// Crate-root access to `MT_NFT_PROCESSOR_NAME`. +pub(crate) use self::materializer::MT_NFT_PROCESSOR_NAME; +/// Crate-root access to `MT_ORACLE_ACCEPTED_FAMILIES`. +pub(crate) use self::materializer::MT_ORACLE_ACCEPTED_FAMILIES; +/// Crate-root access to `MT_ORACLE_COMPONENT_NAME`. +pub(crate) use self::materializer::MT_ORACLE_COMPONENT_NAME; +/// Crate-root access to `MT_ORACLE_PROCESSOR_NAME`. +pub(crate) use self::materializer::MT_ORACLE_PROCESSOR_NAME; +/// Crate-root access to `MT_ORDERBOOK_ACCEPTED_FAMILIES`. +pub(crate) use self::materializer::MT_ORDERBOOK_ACCEPTED_FAMILIES; +/// Crate-root access to `MT_ORDERBOOK_COMPONENT_NAME`. +pub(crate) use self::materializer::MT_ORDERBOOK_COMPONENT_NAME; +/// Crate-root access to `MT_ORDERBOOK_PROCESSOR_NAME`. +pub(crate) use self::materializer::MT_ORDERBOOK_PROCESSOR_NAME; +/// Crate-root access to `MT_PERPETUALS_ACCEPTED_FAMILIES`. +pub(crate) use self::materializer::MT_PERPETUALS_ACCEPTED_FAMILIES; +/// Crate-root access to `MT_PERPETUALS_COMPONENT_NAME`. +pub(crate) use self::materializer::MT_PERPETUALS_COMPONENT_NAME; +/// Crate-root access to `MT_PERPETUALS_PROCESSOR_NAME`. +pub(crate) use self::materializer::MT_PERPETUALS_PROCESSOR_NAME; +/// Crate-root access to `MT_POOL_STATE_ACCEPTED_FAMILIES`. +pub(crate) use self::materializer::MT_POOL_STATE_ACCEPTED_FAMILIES; +/// Crate-root access to `MT_POOL_STATE_COMPONENT_NAME`. +pub(crate) use self::materializer::MT_POOL_STATE_COMPONENT_NAME; +/// Crate-root access to `MT_POOL_STATE_PROCESSOR_NAME`. +pub(crate) use self::materializer::MT_POOL_STATE_PROCESSOR_NAME; +/// Crate-root access to `MT_REWARDS_ACCEPTED_FAMILIES`. +pub(crate) use self::materializer::MT_REWARDS_ACCEPTED_FAMILIES; +/// Crate-root access to `MT_REWARDS_COMPONENT_NAME`. +pub(crate) use self::materializer::MT_REWARDS_COMPONENT_NAME; +/// Crate-root access to `MT_REWARDS_PROCESSOR_NAME`. +pub(crate) use self::materializer::MT_REWARDS_PROCESSOR_NAME; +/// Crate-root access to `MT_RISK_ACCEPTED_FAMILIES`. +pub(crate) use self::materializer::MT_RISK_ACCEPTED_FAMILIES; +/// Crate-root access to `MT_RISK_COMPONENT_NAME`. +pub(crate) use self::materializer::MT_RISK_COMPONENT_NAME; +/// Crate-root access to `MT_RISK_PROCESSOR_NAME`. pub(crate) use self::materializer::MT_RISK_PROCESSOR_NAME; -/// Stable projection contract version for SPL Token and ATA risk facts. +/// Crate-root access to `MT_RISK_PROJECTION_VERSION`. pub(crate) use self::materializer::MT_RISK_PROJECTION_VERSION; -/// Stable processor name for Token, ATA and Token-2022 account projections. +/// Crate-root access to `MT_RISK_RISK_ENTRIES`. +pub(crate) use self::materializer::MT_RISK_RISK_ENTRIES; +/// Crate-root access to `MT_ROUTING_ACCEPTED_FAMILIES`. +pub(crate) use self::materializer::MT_ROUTING_ACCEPTED_FAMILIES; +/// Crate-root access to `MT_ROUTING_COMPONENT_NAME`. +pub(crate) use self::materializer::MT_ROUTING_COMPONENT_NAME; +/// Crate-root access to `MT_ROUTING_PROCESSOR_NAME`. +pub(crate) use self::materializer::MT_ROUTING_PROCESSOR_NAME; +/// Crate-root access to `MT_STAKING_ACCEPTED_FAMILIES`. +pub(crate) use self::materializer::MT_STAKING_ACCEPTED_FAMILIES; +/// Crate-root access to `MT_STAKING_COMPONENT_NAME`. +pub(crate) use self::materializer::MT_STAKING_COMPONENT_NAME; +/// Crate-root access to `MT_STAKING_PROCESSOR_NAME`. +pub(crate) use self::materializer::MT_STAKING_PROCESSOR_NAME; +/// Crate-root access to `MT_STAKING_PROJECTION_VERSION`. +pub(crate) use self::materializer::MT_STAKING_PROJECTION_VERSION; +/// Crate-root access to `MT_STAKING_STAKE_ACCOUNT_ENTRIES`. +pub(crate) use self::materializer::MT_STAKING_STAKE_ACCOUNT_ENTRIES; +/// Crate-root access to `MT_STAKING_STAKE_AUTHORITY_ENTRIES`. +pub(crate) use self::materializer::MT_STAKING_STAKE_AUTHORITY_ENTRIES; +/// Crate-root access to `MT_STAKING_STAKE_LOCKUP_ENTRIES`. +pub(crate) use self::materializer::MT_STAKING_STAKE_LOCKUP_ENTRIES; +/// Crate-root access to `MT_STAKING_STAKE_SURFACE`. +pub(crate) use self::materializer::MT_STAKING_STAKE_SURFACE; +/// Crate-root access to `MT_STAKING_STAKE_VALUE_ENTRIES`. +pub(crate) use self::materializer::MT_STAKING_STAKE_VALUE_ENTRIES; +/// Crate-root access to `MT_STAKING_VOTE_ACCOUNT_ENTRIES`. +pub(crate) use self::materializer::MT_STAKING_VOTE_ACCOUNT_ENTRIES; +/// Crate-root access to `MT_STAKING_VOTE_ADMIN_ENTRIES`. +pub(crate) use self::materializer::MT_STAKING_VOTE_ADMIN_ENTRIES; +/// Crate-root access to `MT_STAKING_VOTE_AUTHORITY_ENTRIES`. +pub(crate) use self::materializer::MT_STAKING_VOTE_AUTHORITY_ENTRIES; +/// Crate-root access to `MT_STAKING_VOTE_REWARD_ENTRIES`. +pub(crate) use self::materializer::MT_STAKING_VOTE_REWARD_ENTRIES; +/// Crate-root access to `MT_STAKING_VOTE_STATE_ENTRIES`. +pub(crate) use self::materializer::MT_STAKING_VOTE_STATE_ENTRIES; +/// Crate-root access to `MT_STAKING_VOTE_SURFACE`. +pub(crate) use self::materializer::MT_STAKING_VOTE_SURFACE; +/// Crate-root access to `MT_STAKING_VOTE_VALUE_ENTRIES`. +pub(crate) use self::materializer::MT_STAKING_VOTE_VALUE_ENTRIES; +/// Crate-root access to `MT_TOKEN_ACCOUNTS_ACCEPTED_FAMILIES`. +pub(crate) use self::materializer::MT_TOKEN_ACCOUNTS_ACCEPTED_FAMILIES; +/// Crate-root access to `MT_TOKEN_ACCOUNTS_ATA_LIFECYCLE_ENTRIES`. +pub(crate) use self::materializer::MT_TOKEN_ACCOUNTS_ATA_LIFECYCLE_ENTRIES; +/// Crate-root access to `MT_TOKEN_ACCOUNTS_COMPONENT_NAME`. +pub(crate) use self::materializer::MT_TOKEN_ACCOUNTS_COMPONENT_NAME; +/// Crate-root access to `MT_TOKEN_ACCOUNTS_MUTATION_ENTRIES`. +pub(crate) use self::materializer::MT_TOKEN_ACCOUNTS_MUTATION_ENTRIES; +/// Crate-root access to `MT_TOKEN_ACCOUNTS_PROCESSOR_NAME`. pub(crate) use self::materializer::MT_TOKEN_ACCOUNTS_PROCESSOR_NAME; -/// Stable projection contract version for Token, ATA and Token-2022 account projections. +/// Crate-root access to `MT_TOKEN_ACCOUNTS_PROJECTION_VERSION`. pub(crate) use self::materializer::MT_TOKEN_ACCOUNTS_PROJECTION_VERSION; -/// Maximum Memo payload accepted by transaction annotations. +/// Crate-root access to `MT_TOKEN_METADATA_RISK_ACCEPTED_FAMILIES`. +pub(crate) use self::materializer::MT_TOKEN_METADATA_RISK_ACCEPTED_FAMILIES; +/// Crate-root access to `MT_TOKEN_METADATA_RISK_COMPONENT_NAME`. +pub(crate) use self::materializer::MT_TOKEN_METADATA_RISK_COMPONENT_NAME; +/// Crate-root access to `MT_TOKEN_METADATA_RISK_PROCESSOR_NAME`. +pub(crate) use self::materializer::MT_TOKEN_METADATA_RISK_PROCESSOR_NAME; +/// Crate-root access to `MT_TRADES_ACCEPTED_FAMILIES`. +pub(crate) use self::materializer::MT_TRADES_ACCEPTED_FAMILIES; +/// Crate-root access to `MT_TRADES_COMPONENT_NAME`. +pub(crate) use self::materializer::MT_TRADES_COMPONENT_NAME; +/// Crate-root access to `MT_TRADES_PROCESSOR_NAME`. +pub(crate) use self::materializer::MT_TRADES_PROCESSOR_NAME; +/// Crate-root access to `MT_TRANSACTION_ANNOTATIONS_ACCEPTED_FAMILIES`. +pub(crate) use self::materializer::MT_TRANSACTION_ANNOTATIONS_ACCEPTED_FAMILIES; +/// Crate-root access to `MT_TRANSACTION_ANNOTATIONS_COMPONENT_NAME`. +pub(crate) use self::materializer::MT_TRANSACTION_ANNOTATIONS_COMPONENT_NAME; +/// Crate-root access to `MT_TRANSACTION_ANNOTATIONS_MAX_MEMO_PAYLOAD_BYTES`. pub(crate) use self::materializer::MT_TRANSACTION_ANNOTATIONS_MAX_MEMO_PAYLOAD_BYTES; -/// Stable processor name for transaction annotations. +/// Crate-root access to `MT_TRANSACTION_ANNOTATIONS_MEMO_ENTRY_NAMES`. +pub(crate) use self::materializer::MT_TRANSACTION_ANNOTATIONS_MEMO_ENTRY_NAMES; +/// Crate-root access to `MT_TRANSACTION_ANNOTATIONS_PROCESSOR_NAME`. pub(crate) use self::materializer::MT_TRANSACTION_ANNOTATIONS_PROCESSOR_NAME; -/// Stable projection contract version for transaction annotations. +/// Crate-root access to `MT_TRANSACTION_ANNOTATIONS_PROJECTION_VERSION`. pub(crate) use self::materializer::MT_TRANSACTION_ANNOTATIONS_PROJECTION_VERSION; -/// Canonical tracing target for the native and SPL administration materializer. +/// Crate-root access to `MT_VAULT_ACCEPTED_FAMILIES`. +pub(crate) use self::materializer::MT_VAULT_ACCEPTED_FAMILIES; +/// Crate-root access to `MT_VAULT_COMPONENT_NAME`. +pub(crate) use self::materializer::MT_VAULT_COMPONENT_NAME; +/// Crate-root access to `MT_VAULT_PROCESSOR_NAME`. +pub(crate) use self::materializer::MT_VAULT_PROCESSOR_NAME; +/// Crate-root access to `TRACING_TARGET_MATERIALIZER_ADMIN`. pub(crate) use self::materializer::TRACING_TARGET_MATERIALIZER_ADMIN; -/// Canonical tracing target for the native compliance audit materializer. +/// Crate-root access to `TRACING_TARGET_MATERIALIZER_COMPLIANCE_AUDIT`. pub(crate) use self::materializer::TRACING_TARGET_MATERIALIZER_COMPLIANCE_AUDIT; -/// Canonical tracing target for the native lifecycle materializer. +/// Crate-root access to `TRACING_TARGET_MATERIALIZER_FEES`. +pub(crate) use self::materializer::TRACING_TARGET_MATERIALIZER_FEES; +/// Crate-root access to `TRACING_TARGET_MATERIALIZER_LIFECYCLE`. pub(crate) use self::materializer::TRACING_TARGET_MATERIALIZER_LIFECYCLE; -/// Canonical tracing target for Solana Program Metadata materialization. +/// Crate-root access to `TRACING_TARGET_MATERIALIZER_METADATA_METAPLEX_TOKEN_METADATA`. +pub(crate) use self::materializer::TRACING_TARGET_MATERIALIZER_METADATA_METAPLEX_TOKEN_METADATA; +/// Crate-root access to `TRACING_TARGET_MATERIALIZER_METADATA_SOLANA_PROGRAM_METADATA`. pub(crate) use self::materializer::TRACING_TARGET_MATERIALIZER_METADATA_SOLANA_PROGRAM_METADATA; -/// Canonical tracing target for SPL Token and ATA risk facts. +/// Crate-root access to `TRACING_TARGET_MATERIALIZER_METADATA_TOKEN_2022`. +pub(crate) use self::materializer::TRACING_TARGET_MATERIALIZER_METADATA_TOKEN_2022; +/// Crate-root access to `TRACING_TARGET_MATERIALIZER_RISK`. pub(crate) use self::materializer::TRACING_TARGET_MATERIALIZER_RISK; -/// Canonical tracing target for the native stake and vote materializer. +/// Crate-root access to `TRACING_TARGET_MATERIALIZER_STAKING`. pub(crate) use self::materializer::TRACING_TARGET_MATERIALIZER_STAKING; -/// Canonical tracing target for Token, ATA and Token-2022 account projections. +/// Crate-root access to `TRACING_TARGET_MATERIALIZER_TOKEN_ACCOUNTS`. pub(crate) use self::materializer::TRACING_TARGET_MATERIALIZER_TOKEN_ACCOUNTS; -/// Canonical tracing target for transaction annotations. +/// Crate-root access to `TRACING_TARGET_MATERIALIZER_TRANSACTION_ANNOTATIONS`. pub(crate) use self::materializer::TRACING_TARGET_MATERIALIZER_TRANSACTION_ANNOTATIONS; diff --git a/kb-lib/src/materializer.rs b/kb-lib/src/materializer.rs index ce9fb7b..23fc8af 100644 --- a/kb-lib/src/materializer.rs +++ b/kb-lib/src/materializer.rs @@ -1,5 +1,5 @@ // file: kb-lib/src/materializer.rs -// version: 9 +// version: 14 //! Consolidated materializer modules. @@ -73,8 +73,10 @@ pub use self::lending::MtLendingMaterializer; pub use self::lifecycle::MtLifecycleMaterializer; /// Reserved liquidity materializer. pub use self::liquidity::MtLiquidityMaterializer; -/// Token-2022, Metaplex and Solana Program Metadata materializer. +/// Compatibility wrapper for the Solana Program Metadata materializer. pub use self::metadata::MtMetadataMaterializer; +/// Stable Solana Program Metadata instruction materializer. +pub use self::metadata::MtSolanaProgramMetadataMaterializer; /// Materializes one canonical non-Metadata Metaplex account snapshot. pub use self::metadata::materializer_metadata_materialize_metaplex_account_snapshot; /// Materializes one canonical Metaplex Metadata account snapshot. @@ -118,100 +120,533 @@ pub use self::transaction::MtTransactionAnnotationMaterializer; /// Reserved vault materializer. pub use self::vault::MtVaultMaterializer; -/// Canonical processor name for the native and SPL administration materializer. +/// Crate-visible `MT_ADMIN_ACCEPTED_FAMILIES` component constant. +pub(crate) use self::admin::MT_ADMIN_ACCEPTED_FAMILIES; +/// Crate-visible `MT_ADMIN_BPF_LOADER_UPGRADEABLE_ENTRIES` component constant. +pub(crate) use self::admin::MT_ADMIN_BPF_LOADER_UPGRADEABLE_ENTRIES; +/// Crate-visible `MT_ADMIN_BPF_LOADER_UPGRADEABLE_SURFACE` component constant. +pub(crate) use self::admin::MT_ADMIN_BPF_LOADER_UPGRADEABLE_SURFACE; +/// Crate-visible `MT_ADMIN_COMPONENT_NAME` component constant. +pub(crate) use self::admin::MT_ADMIN_COMPONENT_NAME; +/// Crate-visible `MT_ADMIN_CONFIG_ENTRIES` component constant. +pub(crate) use self::admin::MT_ADMIN_CONFIG_ENTRIES; +/// Crate-visible `MT_ADMIN_CONFIG_SURFACE` component constant. +pub(crate) use self::admin::MT_ADMIN_CONFIG_SURFACE; +/// Crate-visible `MT_ADMIN_LOADER_V4_ENTRIES` component constant. +pub(crate) use self::admin::MT_ADMIN_LOADER_V4_ENTRIES; +/// Crate-visible `MT_ADMIN_LOADER_V4_SURFACE` component constant. +pub(crate) use self::admin::MT_ADMIN_LOADER_V4_SURFACE; +/// Crate-visible `MT_ADMIN_PROCESSOR_NAME` component constant. pub(crate) use self::admin::MT_ADMIN_PROCESSOR_NAME; -/// Canonical tracing target for the native and SPL administration materializer. +/// Crate-visible `MT_ADMIN_PROJECTION_VERSION` component constant. +pub(crate) use self::admin::MT_ADMIN_PROJECTION_VERSION; +/// Crate-visible `MT_ADMIN_SPL_ELGAMAL_REGISTRY_ENTRIES` component constant. +pub(crate) use self::admin::MT_ADMIN_SPL_ELGAMAL_REGISTRY_ENTRIES; +/// Crate-visible `MT_ADMIN_SPL_ELGAMAL_REGISTRY_SURFACE` component constant. +pub(crate) use self::admin::MT_ADMIN_SPL_ELGAMAL_REGISTRY_SURFACE; +/// Crate-visible `MT_ADMIN_SPL_TOKEN_2022_EXTENSION_ADMIN_ENTRIES` component constant. +pub(crate) use self::admin::MT_ADMIN_SPL_TOKEN_2022_EXTENSION_ADMIN_ENTRIES; +/// Crate-visible `MT_ADMIN_SPL_TOKEN_2022_SURFACE` component constant. +pub(crate) use self::admin::MT_ADMIN_SPL_TOKEN_2022_SURFACE; +/// Crate-visible `MT_ADMIN_SPL_TOKEN_AUTHORITY_ENTRIES` component constant. +pub(crate) use self::admin::MT_ADMIN_SPL_TOKEN_AUTHORITY_ENTRIES; +/// Crate-visible `MT_ADMIN_SPL_TOKEN_MULTISIG_ENTRIES` component constant. +pub(crate) use self::admin::MT_ADMIN_SPL_TOKEN_MULTISIG_ENTRIES; +/// Crate-visible `MT_ADMIN_SPL_TOKEN_SURFACE` component constant. +pub(crate) use self::admin::MT_ADMIN_SPL_TOKEN_SURFACE; +/// Crate-visible `MT_ADMIN_SYSTEM_ENTRIES` component constant. +pub(crate) use self::admin::MT_ADMIN_SYSTEM_ENTRIES; +/// Crate-visible `MT_ADMIN_SYSTEM_SURFACE` component constant. +pub(crate) use self::admin::MT_ADMIN_SYSTEM_SURFACE; +/// Crate-visible `TRACING_TARGET_MATERIALIZER_ADMIN` component constant. pub(crate) use self::admin::TRACING_TARGET_MATERIALIZER_ADMIN; -/// Canonical tracing target for the native compliance audit materializer. +/// Crate-visible `MT_BRIDGE_ACCEPTED_FAMILIES` component constant. +pub(crate) use self::bridge::MT_BRIDGE_ACCEPTED_FAMILIES; +/// Crate-visible `MT_BRIDGE_COMPONENT_NAME` component constant. +pub(crate) use self::bridge::MT_BRIDGE_COMPONENT_NAME; +/// Crate-visible `MT_BRIDGE_PROCESSOR_NAME` component constant. +pub(crate) use self::bridge::MT_BRIDGE_PROCESSOR_NAME; +/// Crate-visible `MT_COMPLIANCE_AUDIT_ACCEPTED_FAMILIES` component constant. +pub(crate) use self::compliance::MT_COMPLIANCE_AUDIT_ACCEPTED_FAMILIES; +/// Crate-visible `MT_COMPLIANCE_AUDIT_BPF_LOADER_DEPRECATED_SURFACE` component constant. +pub(crate) use self::compliance::MT_COMPLIANCE_AUDIT_BPF_LOADER_DEPRECATED_SURFACE; +/// Crate-visible `MT_COMPLIANCE_AUDIT_BPF_LOADER_SURFACE` component constant. +pub(crate) use self::compliance::MT_COMPLIANCE_AUDIT_BPF_LOADER_SURFACE; +/// Crate-visible `MT_COMPLIANCE_AUDIT_BPF_LOADER_UPGRADEABLE_SURFACE` component constant. +pub(crate) use self::compliance::MT_COMPLIANCE_AUDIT_BPF_LOADER_UPGRADEABLE_SURFACE; +/// Crate-visible `MT_COMPLIANCE_AUDIT_COMPONENT_NAME` component constant. +pub(crate) use self::compliance::MT_COMPLIANCE_AUDIT_COMPONENT_NAME; +/// Crate-visible `MT_COMPLIANCE_AUDIT_COMPUTE_BUDGET_ENTRIES` component constant. +pub(crate) use self::compliance::MT_COMPLIANCE_AUDIT_COMPUTE_BUDGET_ENTRIES; +/// Crate-visible `MT_COMPLIANCE_AUDIT_COMPUTE_BUDGET_SURFACE` component constant. +pub(crate) use self::compliance::MT_COMPLIANCE_AUDIT_COMPUTE_BUDGET_SURFACE; +/// Crate-visible `MT_COMPLIANCE_AUDIT_LOADER_V4_COPY_ENTRIES` component constant. +pub(crate) use self::compliance::MT_COMPLIANCE_AUDIT_LOADER_V4_COPY_ENTRIES; +/// Crate-visible `MT_COMPLIANCE_AUDIT_LOADER_V4_SURFACE` component constant. +pub(crate) use self::compliance::MT_COMPLIANCE_AUDIT_LOADER_V4_SURFACE; +/// Crate-visible `MT_COMPLIANCE_AUDIT_PROCESSOR_NAME` component constant. +pub(crate) use self::compliance::MT_COMPLIANCE_AUDIT_PROCESSOR_NAME; +/// Crate-visible `MT_COMPLIANCE_AUDIT_PROJECTION_VERSION` component constant. +pub(crate) use self::compliance::MT_COMPLIANCE_AUDIT_PROJECTION_VERSION; +/// Crate-visible `MT_COMPLIANCE_AUDIT_WRITE_ENTRIES` component constant. +pub(crate) use self::compliance::MT_COMPLIANCE_AUDIT_WRITE_ENTRIES; +/// Crate-visible `TRACING_TARGET_MATERIALIZER_COMPLIANCE_AUDIT` component constant. pub(crate) use self::compliance::TRACING_TARGET_MATERIALIZER_COMPLIANCE_AUDIT; -/// Canonical tracing target for the native lifecycle materializer. +/// Crate-visible `MT_FEES_ACCEPTED_FAMILIES` component constant. +pub(crate) use self::fees::MT_FEES_ACCEPTED_FAMILIES; +/// Crate-visible `MT_FEES_COMPONENT_NAME` component constant. +pub(crate) use self::fees::MT_FEES_COMPONENT_NAME; +/// Crate-visible `MT_FEES_PROCESSOR_NAME` component constant. +pub(crate) use self::fees::MT_FEES_PROCESSOR_NAME; +/// Crate-visible `MT_FEES_PROJECTION_VERSION` component constant. +pub(crate) use self::fees::MT_FEES_PROJECTION_VERSION; +/// Crate-visible `MT_FEES_SPL_TOKEN_2022_SURFACE` component constant. +pub(crate) use self::fees::MT_FEES_SPL_TOKEN_2022_SURFACE; +/// Crate-visible `MT_FEES_TOKEN_2022_FEE_ENTRIES` component constant. +pub(crate) use self::fees::MT_FEES_TOKEN_2022_FEE_ENTRIES; +/// Crate-visible `TRACING_TARGET_MATERIALIZER_FEES` component constant. +pub(crate) use self::fees::TRACING_TARGET_MATERIALIZER_FEES; +/// Crate-visible `MT_GOVERNANCE_ACCEPTED_FAMILIES` component constant. +pub(crate) use self::governance::MT_GOVERNANCE_ACCEPTED_FAMILIES; +/// Crate-visible `MT_GOVERNANCE_COMPONENT_NAME` component constant. +pub(crate) use self::governance::MT_GOVERNANCE_COMPONENT_NAME; +/// Crate-visible `MT_GOVERNANCE_PROCESSOR_NAME` component constant. +pub(crate) use self::governance::MT_GOVERNANCE_PROCESSOR_NAME; +/// Crate-visible `MT_LENDING_ACCEPTED_FAMILIES` component constant. +pub(crate) use self::lending::MT_LENDING_ACCEPTED_FAMILIES; +/// Crate-visible `MT_LENDING_COMPONENT_NAME` component constant. +pub(crate) use self::lending::MT_LENDING_COMPONENT_NAME; +/// Crate-visible `MT_LENDING_PROCESSOR_NAME` component constant. +pub(crate) use self::lending::MT_LENDING_PROCESSOR_NAME; +/// Crate-visible `MT_LIFECYCLE_ACCEPTED_FAMILIES` component constant. +pub(crate) use self::lifecycle::MT_LIFECYCLE_ACCEPTED_FAMILIES; +/// Crate-visible `MT_LIFECYCLE_ADDRESS_LOOKUP_TABLE_ENTRIES` component constant. +pub(crate) use self::lifecycle::MT_LIFECYCLE_ADDRESS_LOOKUP_TABLE_ENTRIES; +/// Crate-visible `MT_LIFECYCLE_ADDRESS_LOOKUP_TABLE_SURFACE` component constant. +pub(crate) use self::lifecycle::MT_LIFECYCLE_ADDRESS_LOOKUP_TABLE_SURFACE; +/// Crate-visible `MT_LIFECYCLE_BPF_LOADER_DEPRECATED_SURFACE` component constant. +pub(crate) use self::lifecycle::MT_LIFECYCLE_BPF_LOADER_DEPRECATED_SURFACE; +/// Crate-visible `MT_LIFECYCLE_BPF_LOADER_SURFACE` component constant. +pub(crate) use self::lifecycle::MT_LIFECYCLE_BPF_LOADER_SURFACE; +/// Crate-visible `MT_LIFECYCLE_BPF_LOADER_UPGRADEABLE_SURFACE` component constant. +pub(crate) use self::lifecycle::MT_LIFECYCLE_BPF_LOADER_UPGRADEABLE_SURFACE; +/// Crate-visible `MT_LIFECYCLE_COMPONENT_NAME` component constant. +pub(crate) use self::lifecycle::MT_LIFECYCLE_COMPONENT_NAME; +/// Crate-visible `MT_LIFECYCLE_FEATURE_ENTRIES` component constant. +pub(crate) use self::lifecycle::MT_LIFECYCLE_FEATURE_ENTRIES; +/// Crate-visible `MT_LIFECYCLE_FEATURE_SURFACE` component constant. +pub(crate) use self::lifecycle::MT_LIFECYCLE_FEATURE_SURFACE; +/// Crate-visible `MT_LIFECYCLE_IMMUTABLE_LOADER_ENTRIES` component constant. +pub(crate) use self::lifecycle::MT_LIFECYCLE_IMMUTABLE_LOADER_ENTRIES; +/// Crate-visible `MT_LIFECYCLE_LOADER_V4_ENTRIES` component constant. +pub(crate) use self::lifecycle::MT_LIFECYCLE_LOADER_V4_ENTRIES; +/// Crate-visible `MT_LIFECYCLE_LOADER_V4_SURFACE` component constant. +pub(crate) use self::lifecycle::MT_LIFECYCLE_LOADER_V4_SURFACE; +/// Crate-visible `MT_LIFECYCLE_PROCESSOR_NAME` component constant. +pub(crate) use self::lifecycle::MT_LIFECYCLE_PROCESSOR_NAME; +/// Crate-visible `MT_LIFECYCLE_PROJECTION_VERSION` component constant. +pub(crate) use self::lifecycle::MT_LIFECYCLE_PROJECTION_VERSION; +/// Crate-visible `MT_LIFECYCLE_SLASHING_ENTRIES` component constant. +pub(crate) use self::lifecycle::MT_LIFECYCLE_SLASHING_ENTRIES; +/// Crate-visible `MT_LIFECYCLE_SLASHING_SURFACE` component constant. +pub(crate) use self::lifecycle::MT_LIFECYCLE_SLASHING_SURFACE; +/// Crate-visible `MT_LIFECYCLE_SYSTEM_ACCOUNT_ENTRIES` component constant. +pub(crate) use self::lifecycle::MT_LIFECYCLE_SYSTEM_ACCOUNT_ENTRIES; +/// Crate-visible `MT_LIFECYCLE_SYSTEM_NONCE_ENTRIES` component constant. +pub(crate) use self::lifecycle::MT_LIFECYCLE_SYSTEM_NONCE_ENTRIES; +/// Crate-visible `MT_LIFECYCLE_SYSTEM_SURFACE` component constant. +pub(crate) use self::lifecycle::MT_LIFECYCLE_SYSTEM_SURFACE; +/// Crate-visible `MT_LIFECYCLE_UPGRADEABLE_LOADER_ENTRIES` component constant. +pub(crate) use self::lifecycle::MT_LIFECYCLE_UPGRADEABLE_LOADER_ENTRIES; +/// Crate-visible `MT_LIFECYCLE_ZK_ELGAMAL_CLOSE_ENTRIES` component constant. +pub(crate) use self::lifecycle::MT_LIFECYCLE_ZK_ELGAMAL_CLOSE_ENTRIES; +/// Crate-visible `MT_LIFECYCLE_ZK_ELGAMAL_PROOF_SURFACE` component constant. +pub(crate) use self::lifecycle::MT_LIFECYCLE_ZK_ELGAMAL_PROOF_SURFACE; +/// Crate-visible `MT_LIFECYCLE_ZK_ELGAMAL_VERIFY_ENTRIES` component constant. +pub(crate) use self::lifecycle::MT_LIFECYCLE_ZK_ELGAMAL_VERIFY_ENTRIES; +/// Crate-visible `TRACING_TARGET_MATERIALIZER_LIFECYCLE` component constant. pub(crate) use self::lifecycle::TRACING_TARGET_MATERIALIZER_LIFECYCLE; -/// Event families accepted by the Solana Program Metadata instruction materializer. -pub(crate) use self::metadata::MT_METADATA_SPM_ACCEPTED_FAMILIES; -/// Stable processor name for Solana Program Metadata projections. -pub(crate) use self::metadata::MT_METADATA_SPM_PROCESSOR_NAME; -/// Stable projection schema version for Solana Program Metadata outputs. -pub(crate) use self::metadata::MT_METADATA_SPM_PROJECTION_VERSION; -/// Canonical tracing target for Solana Program Metadata materialization. +/// Crate-visible `MT_LIQUIDITY_ACCEPTED_FAMILIES` component constant. +pub(crate) use self::liquidity::MT_LIQUIDITY_ACCEPTED_FAMILIES; +/// Crate-visible `MT_LIQUIDITY_COMPONENT_NAME` component constant. +pub(crate) use self::liquidity::MT_LIQUIDITY_COMPONENT_NAME; +/// Crate-visible `MT_LIQUIDITY_PROCESSOR_NAME` component constant. +pub(crate) use self::liquidity::MT_LIQUIDITY_PROCESSOR_NAME; +/// Crate-visible `MT_METADATA_METAPLEX_TOKEN_METADATA_COMPONENT_NAME` component constant. +pub(crate) use self::metadata::MT_METADATA_METAPLEX_TOKEN_METADATA_COMPONENT_NAME; +/// Crate-visible `MT_METADATA_METAPLEX_TOKEN_METADATA_PROCESSOR_NAME` component constant. +pub(crate) use self::metadata::MT_METADATA_METAPLEX_TOKEN_METADATA_PROCESSOR_NAME; +/// Crate-visible `MT_METADATA_METAPLEX_TOKEN_METADATA_PROJECTION_VERSION` component constant. +pub(crate) use self::metadata::MT_METADATA_METAPLEX_TOKEN_METADATA_PROJECTION_VERSION; +/// Crate-visible `MT_METADATA_SOLANA_PROGRAM_METADATA_ACCEPTED_FAMILIES` component constant. +pub(crate) use self::metadata::MT_METADATA_SOLANA_PROGRAM_METADATA_ACCEPTED_FAMILIES; +/// Crate-visible `MT_METADATA_SOLANA_PROGRAM_METADATA_COMPONENT_NAME` component constant. +pub(crate) use self::metadata::MT_METADATA_SOLANA_PROGRAM_METADATA_COMPONENT_NAME; +/// Crate-visible `MT_METADATA_SOLANA_PROGRAM_METADATA_PROCESSOR_NAME` component constant. +pub(crate) use self::metadata::MT_METADATA_SOLANA_PROGRAM_METADATA_PROCESSOR_NAME; +/// Crate-visible `MT_METADATA_SOLANA_PROGRAM_METADATA_PROJECTION_VERSION` component constant. +pub(crate) use self::metadata::MT_METADATA_SOLANA_PROGRAM_METADATA_PROJECTION_VERSION; +/// Crate-visible `MT_METADATA_TOKEN_2022_COMPONENT_NAME` component constant. +pub(crate) use self::metadata::MT_METADATA_TOKEN_2022_COMPONENT_NAME; +/// Crate-visible `MT_METADATA_TOKEN_2022_PROCESSOR_NAME` component constant. +pub(crate) use self::metadata::MT_METADATA_TOKEN_2022_PROCESSOR_NAME; +/// Crate-visible `MT_METADATA_TOKEN_2022_PROJECTION_VERSION` component constant. +pub(crate) use self::metadata::MT_METADATA_TOKEN_2022_PROJECTION_VERSION; +/// Crate-visible `TRACING_TARGET_MATERIALIZER_METADATA_METAPLEX_TOKEN_METADATA` component constant. +pub(crate) use self::metadata::TRACING_TARGET_MATERIALIZER_METADATA_METAPLEX_TOKEN_METADATA; +/// Crate-visible `TRACING_TARGET_MATERIALIZER_METADATA_SOLANA_PROGRAM_METADATA` component constant. pub(crate) use self::metadata::TRACING_TARGET_MATERIALIZER_METADATA_SOLANA_PROGRAM_METADATA; -/// Stable processor name for SPL Token and ATA risk facts. +/// Crate-visible `TRACING_TARGET_MATERIALIZER_METADATA_TOKEN_2022` component constant. +pub(crate) use self::metadata::TRACING_TARGET_MATERIALIZER_METADATA_TOKEN_2022; +/// Crate-visible `MT_NFT_ACCEPTED_FAMILIES` component constant. +pub(crate) use self::nft::MT_NFT_ACCEPTED_FAMILIES; +/// Crate-visible `MT_NFT_COMPONENT_NAME` component constant. +pub(crate) use self::nft::MT_NFT_COMPONENT_NAME; +/// Crate-visible `MT_NFT_PROCESSOR_NAME` component constant. +pub(crate) use self::nft::MT_NFT_PROCESSOR_NAME; +/// Crate-visible `MT_ORACLE_ACCEPTED_FAMILIES` component constant. +pub(crate) use self::oracle::MT_ORACLE_ACCEPTED_FAMILIES; +/// Crate-visible `MT_ORACLE_COMPONENT_NAME` component constant. +pub(crate) use self::oracle::MT_ORACLE_COMPONENT_NAME; +/// Crate-visible `MT_ORACLE_PROCESSOR_NAME` component constant. +pub(crate) use self::oracle::MT_ORACLE_PROCESSOR_NAME; +/// Crate-visible `MT_ORDERBOOK_ACCEPTED_FAMILIES` component constant. +pub(crate) use self::orderbook::MT_ORDERBOOK_ACCEPTED_FAMILIES; +/// Crate-visible `MT_ORDERBOOK_COMPONENT_NAME` component constant. +pub(crate) use self::orderbook::MT_ORDERBOOK_COMPONENT_NAME; +/// Crate-visible `MT_ORDERBOOK_PROCESSOR_NAME` component constant. +pub(crate) use self::orderbook::MT_ORDERBOOK_PROCESSOR_NAME; +/// Crate-visible `MT_PERPETUALS_ACCEPTED_FAMILIES` component constant. +pub(crate) use self::perpetuals::MT_PERPETUALS_ACCEPTED_FAMILIES; +/// Crate-visible `MT_PERPETUALS_COMPONENT_NAME` component constant. +pub(crate) use self::perpetuals::MT_PERPETUALS_COMPONENT_NAME; +/// Crate-visible `MT_PERPETUALS_PROCESSOR_NAME` component constant. +pub(crate) use self::perpetuals::MT_PERPETUALS_PROCESSOR_NAME; +/// Crate-visible `MT_POOL_STATE_ACCEPTED_FAMILIES` component constant. +pub(crate) use self::pool::MT_POOL_STATE_ACCEPTED_FAMILIES; +/// Crate-visible `MT_POOL_STATE_COMPONENT_NAME` component constant. +pub(crate) use self::pool::MT_POOL_STATE_COMPONENT_NAME; +/// Crate-visible `MT_POOL_STATE_PROCESSOR_NAME` component constant. +pub(crate) use self::pool::MT_POOL_STATE_PROCESSOR_NAME; +/// Crate-visible `MT_REWARDS_ACCEPTED_FAMILIES` component constant. +pub(crate) use self::rewards::MT_REWARDS_ACCEPTED_FAMILIES; +/// Crate-visible `MT_REWARDS_COMPONENT_NAME` component constant. +pub(crate) use self::rewards::MT_REWARDS_COMPONENT_NAME; +/// Crate-visible `MT_REWARDS_PROCESSOR_NAME` component constant. +pub(crate) use self::rewards::MT_REWARDS_PROCESSOR_NAME; +/// Crate-visible `MT_RISK_ACCEPTED_FAMILIES` component constant. +pub(crate) use self::risk::MT_RISK_ACCEPTED_FAMILIES; +/// Crate-visible `MT_RISK_COMPONENT_NAME` component constant. +pub(crate) use self::risk::MT_RISK_COMPONENT_NAME; +/// Crate-visible `MT_RISK_PROCESSOR_NAME` component constant. pub(crate) use self::risk::MT_RISK_PROCESSOR_NAME; -/// Stable projection contract version for SPL Token and ATA risk facts. +/// Crate-visible `MT_RISK_PROJECTION_VERSION` component constant. pub(crate) use self::risk::MT_RISK_PROJECTION_VERSION; -/// Canonical tracing target for SPL Token and ATA risk facts. +/// Crate-visible `MT_RISK_RISK_ENTRIES` component constant. +pub(crate) use self::risk::MT_RISK_RISK_ENTRIES; +/// Crate-visible `TRACING_TARGET_MATERIALIZER_RISK` component constant. pub(crate) use self::risk::TRACING_TARGET_MATERIALIZER_RISK; -/// Canonical tracing target for the native stake and vote materializer. +/// Crate-visible `MT_ROUTING_ACCEPTED_FAMILIES` component constant. +pub(crate) use self::routing::MT_ROUTING_ACCEPTED_FAMILIES; +/// Crate-visible `MT_ROUTING_COMPONENT_NAME` component constant. +pub(crate) use self::routing::MT_ROUTING_COMPONENT_NAME; +/// Crate-visible `MT_ROUTING_PROCESSOR_NAME` component constant. +pub(crate) use self::routing::MT_ROUTING_PROCESSOR_NAME; +/// Crate-visible `MT_STAKING_ACCEPTED_FAMILIES` component constant. +pub(crate) use self::staking::MT_STAKING_ACCEPTED_FAMILIES; +/// Crate-visible `MT_STAKING_COMPONENT_NAME` component constant. +pub(crate) use self::staking::MT_STAKING_COMPONENT_NAME; +/// Crate-visible `MT_STAKING_PROCESSOR_NAME` component constant. +pub(crate) use self::staking::MT_STAKING_PROCESSOR_NAME; +/// Crate-visible `MT_STAKING_PROJECTION_VERSION` component constant. +pub(crate) use self::staking::MT_STAKING_PROJECTION_VERSION; +/// Crate-visible `MT_STAKING_STAKE_ACCOUNT_ENTRIES` component constant. +pub(crate) use self::staking::MT_STAKING_STAKE_ACCOUNT_ENTRIES; +/// Crate-visible `MT_STAKING_STAKE_AUTHORITY_ENTRIES` component constant. +pub(crate) use self::staking::MT_STAKING_STAKE_AUTHORITY_ENTRIES; +/// Crate-visible `MT_STAKING_STAKE_LOCKUP_ENTRIES` component constant. +pub(crate) use self::staking::MT_STAKING_STAKE_LOCKUP_ENTRIES; +/// Crate-visible `MT_STAKING_STAKE_SURFACE` component constant. +pub(crate) use self::staking::MT_STAKING_STAKE_SURFACE; +/// Crate-visible `MT_STAKING_STAKE_VALUE_ENTRIES` component constant. +pub(crate) use self::staking::MT_STAKING_STAKE_VALUE_ENTRIES; +/// Crate-visible `MT_STAKING_VOTE_ACCOUNT_ENTRIES` component constant. +pub(crate) use self::staking::MT_STAKING_VOTE_ACCOUNT_ENTRIES; +/// Crate-visible `MT_STAKING_VOTE_ADMIN_ENTRIES` component constant. +pub(crate) use self::staking::MT_STAKING_VOTE_ADMIN_ENTRIES; +/// Crate-visible `MT_STAKING_VOTE_AUTHORITY_ENTRIES` component constant. +pub(crate) use self::staking::MT_STAKING_VOTE_AUTHORITY_ENTRIES; +/// Crate-visible `MT_STAKING_VOTE_REWARD_ENTRIES` component constant. +pub(crate) use self::staking::MT_STAKING_VOTE_REWARD_ENTRIES; +/// Crate-visible `MT_STAKING_VOTE_STATE_ENTRIES` component constant. +pub(crate) use self::staking::MT_STAKING_VOTE_STATE_ENTRIES; +/// Crate-visible `MT_STAKING_VOTE_SURFACE` component constant. +pub(crate) use self::staking::MT_STAKING_VOTE_SURFACE; +/// Crate-visible `MT_STAKING_VOTE_VALUE_ENTRIES` component constant. +pub(crate) use self::staking::MT_STAKING_VOTE_VALUE_ENTRIES; +/// Crate-visible `TRACING_TARGET_MATERIALIZER_STAKING` component constant. pub(crate) use self::staking::TRACING_TARGET_MATERIALIZER_STAKING; -/// Stable processor name for Token, ATA and Token-2022 account projections. +/// Crate-visible `MT_TOKEN_ACCOUNTS_ACCEPTED_FAMILIES` component constant. +pub(crate) use self::token::MT_TOKEN_ACCOUNTS_ACCEPTED_FAMILIES; +/// Crate-visible `MT_TOKEN_ACCOUNTS_ATA_LIFECYCLE_ENTRIES` component constant. +pub(crate) use self::token::MT_TOKEN_ACCOUNTS_ATA_LIFECYCLE_ENTRIES; +/// Crate-visible `MT_TOKEN_ACCOUNTS_COMPONENT_NAME` component constant. +pub(crate) use self::token::MT_TOKEN_ACCOUNTS_COMPONENT_NAME; +/// Crate-visible `MT_TOKEN_ACCOUNTS_MUTATION_ENTRIES` component constant. +pub(crate) use self::token::MT_TOKEN_ACCOUNTS_MUTATION_ENTRIES; +/// Crate-visible `MT_TOKEN_ACCOUNTS_PROCESSOR_NAME` component constant. pub(crate) use self::token::MT_TOKEN_ACCOUNTS_PROCESSOR_NAME; -/// Stable projection contract version for Token, ATA and Token-2022 account projections. +/// Crate-visible `MT_TOKEN_ACCOUNTS_PROJECTION_VERSION` component constant. pub(crate) use self::token::MT_TOKEN_ACCOUNTS_PROJECTION_VERSION; -/// Canonical tracing target for Token, ATA and Token-2022 account projections. +/// Crate-visible `MT_TOKEN_METADATA_RISK_ACCEPTED_FAMILIES` component constant. +pub(crate) use self::token::MT_TOKEN_METADATA_RISK_ACCEPTED_FAMILIES; +/// Crate-visible `MT_TOKEN_METADATA_RISK_COMPONENT_NAME` component constant. +pub(crate) use self::token::MT_TOKEN_METADATA_RISK_COMPONENT_NAME; +/// Crate-visible `MT_TOKEN_METADATA_RISK_PROCESSOR_NAME` component constant. +pub(crate) use self::token::MT_TOKEN_METADATA_RISK_PROCESSOR_NAME; +/// Crate-visible `TRACING_TARGET_MATERIALIZER_TOKEN_ACCOUNTS` component constant. pub(crate) use self::token::TRACING_TARGET_MATERIALIZER_TOKEN_ACCOUNTS; -/// Maximum Memo payload accepted by transaction annotations. +/// Crate-visible `MT_TRADES_ACCEPTED_FAMILIES` component constant. +pub(crate) use self::trades::MT_TRADES_ACCEPTED_FAMILIES; +/// Crate-visible `MT_TRADES_COMPONENT_NAME` component constant. +pub(crate) use self::trades::MT_TRADES_COMPONENT_NAME; +/// Crate-visible `MT_TRADES_PROCESSOR_NAME` component constant. +pub(crate) use self::trades::MT_TRADES_PROCESSOR_NAME; +/// Crate-visible `MT_TRANSACTION_ANNOTATIONS_ACCEPTED_FAMILIES` component constant. +pub(crate) use self::transaction::MT_TRANSACTION_ANNOTATIONS_ACCEPTED_FAMILIES; +/// Crate-visible `MT_TRANSACTION_ANNOTATIONS_COMPONENT_NAME` component constant. +pub(crate) use self::transaction::MT_TRANSACTION_ANNOTATIONS_COMPONENT_NAME; +/// Crate-visible `MT_TRANSACTION_ANNOTATIONS_MAX_MEMO_PAYLOAD_BYTES` component constant. pub(crate) use self::transaction::MT_TRANSACTION_ANNOTATIONS_MAX_MEMO_PAYLOAD_BYTES; -/// Stable processor name for transaction annotations. +/// Crate-visible `MT_TRANSACTION_ANNOTATIONS_MEMO_ENTRY_NAMES` component constant. +pub(crate) use self::transaction::MT_TRANSACTION_ANNOTATIONS_MEMO_ENTRY_NAMES; +/// Crate-visible `MT_TRANSACTION_ANNOTATIONS_PROCESSOR_NAME` component constant. pub(crate) use self::transaction::MT_TRANSACTION_ANNOTATIONS_PROCESSOR_NAME; -/// Stable projection contract version for transaction annotations. +/// Crate-visible `MT_TRANSACTION_ANNOTATIONS_PROJECTION_VERSION` component constant. pub(crate) use self::transaction::MT_TRANSACTION_ANNOTATIONS_PROJECTION_VERSION; -/// Canonical tracing target for transaction annotations. +/// Crate-visible `TRACING_TARGET_MATERIALIZER_TRANSACTION_ANNOTATIONS` component constant. pub(crate) use self::transaction::TRACING_TARGET_MATERIALIZER_TRANSACTION_ANNOTATIONS; +/// Crate-visible `MT_VAULT_ACCEPTED_FAMILIES` component constant. +pub(crate) use self::vault::MT_VAULT_ACCEPTED_FAMILIES; +/// Crate-visible `MT_VAULT_COMPONENT_NAME` component constant. +pub(crate) use self::vault::MT_VAULT_COMPONENT_NAME; +/// Crate-visible `MT_VAULT_PROCESSOR_NAME` component constant. +pub(crate) use self::vault::MT_VAULT_PROCESSOR_NAME; #[cfg(test)] mod tests { - #[test] - fn reserved_materializers_expose_inactive_public_contracts() { - let materializers: &[(&dyn crate::MtApiEventMaterializer, &str)] = &[ - (&crate::MtBridgeMaterializer, "kb-lib.materializer.bridge"), - (&crate::MtGovernanceMaterializer, "kb-lib.materializer.governance"), - (&crate::MtLendingMaterializer, "kb-lib.materializer.lending"), - (&crate::MtLiquidityMaterializer, "kb-lib.materializer.liquidity"), - (&crate::MtNftMaterializer, "kb-lib.materializer.nft"), - (&crate::MtOracleMaterializer, "kb-lib.materializer.oracle"), - (&crate::MtOrderbookMaterializer, "kb-lib.materializer.orderbook"), - (&crate::MtPerpetualsMaterializer, "kb-lib.materializer.perpetuals"), - (&crate::MtPoolStateMaterializer, "kb-lib.materializer.pool.state"), - (&crate::MtRewardsMaterializer, "kb-lib.materializer.rewards"), - (&crate::MtRoutingMaterializer, "kb-lib.materializer.routing"), + fn materializer_contracts() -> &'static [( + &'static dyn crate::MtMaterializer, + &'static dyn crate::MtApiEventMaterializer, + &'static str, + &'static str, + )] { + return &[ + ( + &crate::MtAdminMaterializer, + &crate::MtAdminMaterializer, + crate::MT_ADMIN_COMPONENT_NAME, + crate::MT_ADMIN_PROCESSOR_NAME, + ), + ( + &crate::MtBridgeMaterializer, + &crate::MtBridgeMaterializer, + crate::MT_BRIDGE_COMPONENT_NAME, + crate::MT_BRIDGE_PROCESSOR_NAME, + ), + ( + &crate::MtComplianceAuditMaterializer, + &crate::MtComplianceAuditMaterializer, + crate::MT_COMPLIANCE_AUDIT_COMPONENT_NAME, + crate::MT_COMPLIANCE_AUDIT_PROCESSOR_NAME, + ), + ( + &crate::MtFeesMaterializer, + &crate::MtFeesMaterializer, + crate::MT_FEES_COMPONENT_NAME, + crate::MT_FEES_PROCESSOR_NAME, + ), + ( + &crate::MtGovernanceMaterializer, + &crate::MtGovernanceMaterializer, + crate::MT_GOVERNANCE_COMPONENT_NAME, + crate::MT_GOVERNANCE_PROCESSOR_NAME, + ), + ( + &crate::MtLendingMaterializer, + &crate::MtLendingMaterializer, + crate::MT_LENDING_COMPONENT_NAME, + crate::MT_LENDING_PROCESSOR_NAME, + ), + ( + &crate::MtLifecycleMaterializer, + &crate::MtLifecycleMaterializer, + crate::MT_LIFECYCLE_COMPONENT_NAME, + crate::MT_LIFECYCLE_PROCESSOR_NAME, + ), + ( + &crate::MtLiquidityMaterializer, + &crate::MtLiquidityMaterializer, + crate::MT_LIQUIDITY_COMPONENT_NAME, + crate::MT_LIQUIDITY_PROCESSOR_NAME, + ), + ( + &crate::MtSolanaProgramMetadataMaterializer, + &crate::MtSolanaProgramMetadataMaterializer, + crate::MT_METADATA_SOLANA_PROGRAM_METADATA_COMPONENT_NAME, + crate::MT_METADATA_SOLANA_PROGRAM_METADATA_PROCESSOR_NAME, + ), + ( + &crate::MtNftMaterializer, + &crate::MtNftMaterializer, + crate::MT_NFT_COMPONENT_NAME, + crate::MT_NFT_PROCESSOR_NAME, + ), + ( + &crate::MtOracleMaterializer, + &crate::MtOracleMaterializer, + crate::MT_ORACLE_COMPONENT_NAME, + crate::MT_ORACLE_PROCESSOR_NAME, + ), + ( + &crate::MtOrderbookMaterializer, + &crate::MtOrderbookMaterializer, + crate::MT_ORDERBOOK_COMPONENT_NAME, + crate::MT_ORDERBOOK_PROCESSOR_NAME, + ), + ( + &crate::MtPerpetualsMaterializer, + &crate::MtPerpetualsMaterializer, + crate::MT_PERPETUALS_COMPONENT_NAME, + crate::MT_PERPETUALS_PROCESSOR_NAME, + ), + ( + &crate::MtPoolStateMaterializer, + &crate::MtPoolStateMaterializer, + crate::MT_POOL_STATE_COMPONENT_NAME, + crate::MT_POOL_STATE_PROCESSOR_NAME, + ), + ( + &crate::MtRewardsMaterializer, + &crate::MtRewardsMaterializer, + crate::MT_REWARDS_COMPONENT_NAME, + crate::MT_REWARDS_PROCESSOR_NAME, + ), + ( + &crate::MtRiskMaterializer, + &crate::MtRiskMaterializer, + crate::MT_RISK_COMPONENT_NAME, + crate::MT_RISK_PROCESSOR_NAME, + ), + ( + &crate::MtRoutingMaterializer, + &crate::MtRoutingMaterializer, + crate::MT_ROUTING_COMPONENT_NAME, + crate::MT_ROUTING_PROCESSOR_NAME, + ), + ( + &crate::MtStakingMaterializer, + &crate::MtStakingMaterializer, + crate::MT_STAKING_COMPONENT_NAME, + crate::MT_STAKING_PROCESSOR_NAME, + ), + ( + &crate::MtTokenAccountsMaterializer, + &crate::MtTokenAccountsMaterializer, + crate::MT_TOKEN_ACCOUNTS_COMPONENT_NAME, + crate::MT_TOKEN_ACCOUNTS_PROCESSOR_NAME, + ), ( &crate::MtTokenMetadataRiskMaterializer, - "kb-lib.materializer.token.metadata_risk", + &crate::MtTokenMetadataRiskMaterializer, + crate::MT_TOKEN_METADATA_RISK_COMPONENT_NAME, + crate::MT_TOKEN_METADATA_RISK_PROCESSOR_NAME, + ), + ( + &crate::MtTradesMaterializer, + &crate::MtTradesMaterializer, + crate::MT_TRADES_COMPONENT_NAME, + crate::MT_TRADES_PROCESSOR_NAME, + ), + ( + &crate::MtTransactionAnnotationMaterializer, + &crate::MtTransactionAnnotationMaterializer, + crate::MT_TRANSACTION_ANNOTATIONS_COMPONENT_NAME, + crate::MT_TRANSACTION_ANNOTATIONS_PROCESSOR_NAME, + ), + ( + &crate::MtVaultMaterializer, + &crate::MtVaultMaterializer, + crate::MT_VAULT_COMPONENT_NAME, + crate::MT_VAULT_PROCESSOR_NAME, ), - (&crate::MtTradesMaterializer, "kb-lib.materializer.trades"), - (&crate::MtVaultMaterializer, "kb-lib.materializer.vault"), ]; - for (materializer, expected_name) in materializers { - let identity = materializer.identity(); - assert_eq!(identity.name, *expected_name); - assert!(!identity.version.is_empty()); - assert!(materializer.accepted_families().is_empty()); + } + + fn state_only_materializer_contracts() -> &'static [(&'static str, &'static str, &'static str)] + { + return &[ + ( + crate::MT_METADATA_METAPLEX_TOKEN_METADATA_COMPONENT_NAME, + crate::MT_METADATA_METAPLEX_TOKEN_METADATA_PROCESSOR_NAME, + crate::TRACING_TARGET_MATERIALIZER_METADATA_METAPLEX_TOKEN_METADATA, + ), + ( + crate::MT_METADATA_TOKEN_2022_COMPONENT_NAME, + crate::MT_METADATA_TOKEN_2022_PROCESSOR_NAME, + crate::TRACING_TARGET_MATERIALIZER_METADATA_TOKEN_2022, + ), + ]; + } + + #[test] + fn every_materializer_uses_the_component_and_processor_name_convention() { + for (legacy, contextual, component_name, processor_name) in materializer_contracts() { + assert_eq!(legacy.materializer_name(), *component_name); + assert_eq!(contextual.identity().name, *processor_name); + assert!(component_name.starts_with("kb-lib.materializer.")); + assert!(processor_name.starts_with("materializer.")); + assert_eq!( + component_name.strip_prefix("kb-lib."), + std::option::Option::Some(*processor_name) + ); + assert!(!legacy.materializer_version().is_empty()); + assert!(!contextual.identity().version.is_empty()); } } #[test] - fn reserved_materializers_preserve_legacy_trait_contracts() { - let materializers: &[(&dyn crate::MtMaterializer, &str)] = &[ - (&crate::MtBridgeMaterializer, "kb-lib.materializer.bridge"), - (&crate::MtGovernanceMaterializer, "kb-lib.materializer.governance"), - (&crate::MtLendingMaterializer, "kb-lib.materializer.lending"), - (&crate::MtLiquidityMaterializer, "kb-lib.materializer.liquidity"), - (&crate::MtNftMaterializer, "kb-lib.materializer.nft"), - (&crate::MtOracleMaterializer, "kb-lib.materializer.oracle"), - (&crate::MtOrderbookMaterializer, "kb-lib.materializer.orderbook"), - (&crate::MtPerpetualsMaterializer, "kb-lib.materializer.perpetuals"), - (&crate::MtPoolStateMaterializer, "kb-lib.materializer.pool.state"), - (&crate::MtRewardsMaterializer, "kb-lib.materializer.rewards"), - (&crate::MtRoutingMaterializer, "kb-lib.materializer.routing"), - ( - &crate::MtTokenMetadataRiskMaterializer, - "kb-lib.materializer.token.metadata_risk", - ), - (&crate::MtTradesMaterializer, "kb-lib.materializer.trades"), - (&crate::MtVaultMaterializer, "kb-lib.materializer.vault"), + fn state_only_materializers_keep_runtime_processor_and_tracing_identity_aligned() { + for (component_name, processor_name, tracing_target) in state_only_materializer_contracts() + { + assert_eq!(*tracing_target, *component_name); + assert_eq!( + component_name.strip_prefix("kb-lib."), + std::option::Option::Some(*processor_name) + ); + } + } + + #[test] + fn reserved_materializers_are_empty_shells() { + let materializers: &[&dyn crate::MtApiEventMaterializer] = &[ + &crate::MtBridgeMaterializer, + &crate::MtGovernanceMaterializer, + &crate::MtLendingMaterializer, + &crate::MtLiquidityMaterializer, + &crate::MtNftMaterializer, + &crate::MtOracleMaterializer, + &crate::MtOrderbookMaterializer, + &crate::MtPerpetualsMaterializer, + &crate::MtPoolStateMaterializer, + &crate::MtRewardsMaterializer, + &crate::MtRoutingMaterializer, + &crate::MtTokenMetadataRiskMaterializer, + &crate::MtTradesMaterializer, + &crate::MtVaultMaterializer, ]; - for (materializer, expected_name) in materializers { - assert_eq!(materializer.materializer_name(), *expected_name); - assert!(!materializer.materializer_version().is_empty()); + for materializer in materializers { + assert!(materializer.accepted_families().is_empty()); } } } diff --git a/kb-lib/src/materializer/admin.rs b/kb-lib/src/materializer/admin.rs index e2263f8..2f570c4 100644 --- a/kb-lib/src/materializer/admin.rs +++ b/kb-lib/src/materializer/admin.rs @@ -1,19 +1,55 @@ // file: kb-lib/src/materializer/admin.rs -// version: 7 +// version: 8 //! `admin` materializer family. mod constants; -mod core; +mod materializer; /// Stable native and SPL administration materializer. -pub use self::core::MtAdminMaterializer; +pub use self::materializer::MtAdminMaterializer; /// Materializes one exact SPL ElGamal registry account snapshot. -pub use self::core::materializer_admin_materialize_elgamal_registry_state_snapshot; +pub use self::materializer::materializer_admin_materialize_elgamal_registry_state_snapshot; /// Materializes exact Token-2022 administration account snapshots. -pub use self::core::materializer_admin_materialize_token_2022_state_snapshots; +pub use self::materializer::materializer_admin_materialize_token_2022_state_snapshots; -/// Canonical processor name for the native and SPL administration materializer. +/// Crate-visible `MT_ADMIN_ACCEPTED_FAMILIES` component constant. +pub(crate) use self::constants::MT_ADMIN_ACCEPTED_FAMILIES; +/// Crate-visible `MT_ADMIN_BPF_LOADER_UPGRADEABLE_ENTRIES` component constant. +pub(crate) use self::constants::MT_ADMIN_BPF_LOADER_UPGRADEABLE_ENTRIES; +/// Crate-visible `MT_ADMIN_BPF_LOADER_UPGRADEABLE_SURFACE` component constant. +pub(crate) use self::constants::MT_ADMIN_BPF_LOADER_UPGRADEABLE_SURFACE; +/// Stable runtime component name for the `admin` materializer. +pub(crate) use self::constants::MT_ADMIN_COMPONENT_NAME; +/// Crate-visible `MT_ADMIN_CONFIG_ENTRIES` component constant. +pub(crate) use self::constants::MT_ADMIN_CONFIG_ENTRIES; +/// Crate-visible `MT_ADMIN_CONFIG_SURFACE` component constant. +pub(crate) use self::constants::MT_ADMIN_CONFIG_SURFACE; +/// Crate-visible `MT_ADMIN_LOADER_V4_ENTRIES` component constant. +pub(crate) use self::constants::MT_ADMIN_LOADER_V4_ENTRIES; +/// Crate-visible `MT_ADMIN_LOADER_V4_SURFACE` component constant. +pub(crate) use self::constants::MT_ADMIN_LOADER_V4_SURFACE; +/// Stable processor name for the `admin` materializer. pub(crate) use self::constants::MT_ADMIN_PROCESSOR_NAME; -/// Canonical tracing target for the native and SPL administration materializer. +/// Stable projection version for the `admin` materializer. +pub(crate) use self::constants::MT_ADMIN_PROJECTION_VERSION; +/// Crate-visible `MT_ADMIN_SPL_ELGAMAL_REGISTRY_ENTRIES` component constant. +pub(crate) use self::constants::MT_ADMIN_SPL_ELGAMAL_REGISTRY_ENTRIES; +/// Crate-visible `MT_ADMIN_SPL_ELGAMAL_REGISTRY_SURFACE` component constant. +pub(crate) use self::constants::MT_ADMIN_SPL_ELGAMAL_REGISTRY_SURFACE; +/// Crate-visible `MT_ADMIN_SPL_TOKEN_2022_EXTENSION_ADMIN_ENTRIES` component constant. +pub(crate) use self::constants::MT_ADMIN_SPL_TOKEN_2022_EXTENSION_ADMIN_ENTRIES; +/// Crate-visible `MT_ADMIN_SPL_TOKEN_2022_SURFACE` component constant. +pub(crate) use self::constants::MT_ADMIN_SPL_TOKEN_2022_SURFACE; +/// Crate-visible `MT_ADMIN_SPL_TOKEN_AUTHORITY_ENTRIES` component constant. +pub(crate) use self::constants::MT_ADMIN_SPL_TOKEN_AUTHORITY_ENTRIES; +/// Crate-visible `MT_ADMIN_SPL_TOKEN_MULTISIG_ENTRIES` component constant. +pub(crate) use self::constants::MT_ADMIN_SPL_TOKEN_MULTISIG_ENTRIES; +/// Crate-visible `MT_ADMIN_SPL_TOKEN_SURFACE` component constant. +pub(crate) use self::constants::MT_ADMIN_SPL_TOKEN_SURFACE; +/// Crate-visible `MT_ADMIN_SYSTEM_ENTRIES` component constant. +pub(crate) use self::constants::MT_ADMIN_SYSTEM_ENTRIES; +/// Crate-visible `MT_ADMIN_SYSTEM_SURFACE` component constant. +pub(crate) use self::constants::MT_ADMIN_SYSTEM_SURFACE; +/// Canonical tracing target for the `admin` materializer. pub(crate) use self::constants::TRACING_TARGET_MATERIALIZER_ADMIN; diff --git a/kb-lib/src/materializer/admin/constants.rs b/kb-lib/src/materializer/admin/constants.rs index 2837700..164184d 100644 --- a/kb-lib/src/materializer/admin/constants.rs +++ b/kb-lib/src/materializer/admin/constants.rs @@ -1,9 +1,67 @@ // file: kb-lib/src/materializer/admin/constants.rs -// version: 3 +// version: 5 -//! Local constants for the native and SPL administration materializer. +//! Local constants for the admin materializer. -/// Canonical processor name for this component. +/// Stable runtime component name for the admin materializer. +pub(crate) const MT_ADMIN_COMPONENT_NAME: &str = "kb-lib.materializer.admin"; +/// Stable processor name persisted by the replay ledger. pub(crate) const MT_ADMIN_PROCESSOR_NAME: &str = "materializer.admin"; -/// Canonical tracing target for this component. +/// Stable projection contract version for the admin materializer. +pub(crate) const MT_ADMIN_PROJECTION_VERSION: u32 = 2; +/// Canonical tracing target for the admin materializer. pub(crate) const TRACING_TARGET_MATERIALIZER_ADMIN: &str = "kb-lib.materializer.admin"; + +/// Component contract for `accepted_families`. +pub(crate) const MT_ADMIN_ACCEPTED_FAMILIES: &[crate::MdEventFamily] = + &[crate::MdEventFamily::Admin, crate::MdEventFamily::TokenAccount]; +/// Component contract for `system_surface`. +pub(crate) const MT_ADMIN_SYSTEM_SURFACE: &str = "solana.core.system"; +/// Component contract for `system_entries`. +pub(crate) const MT_ADMIN_SYSTEM_ENTRIES: &[&str] = &["assign", "assign_with_seed"]; +/// Component contract for `config_surface`. +pub(crate) const MT_ADMIN_CONFIG_SURFACE: &str = "solana.core.config"; +/// Component contract for `config_entries`. +pub(crate) const MT_ADMIN_CONFIG_ENTRIES: &[&str] = &["store"]; +/// Component contract for `bpf_loader_upgradeable_surface`. +pub(crate) const MT_ADMIN_BPF_LOADER_UPGRADEABLE_SURFACE: &str = + "solana.core.bpf_loader_upgradeable"; +/// Component contract for `bpf_loader_upgradeable_entries`. +pub(crate) const MT_ADMIN_BPF_LOADER_UPGRADEABLE_ENTRIES: &[&str] = + &["set_authority", "set_authority_checked"]; +/// Component contract for `loader_v4_surface`. +pub(crate) const MT_ADMIN_LOADER_V4_SURFACE: &str = "solana.core.loader_v4"; +/// Component contract for `loader_v4_entries`. +pub(crate) const MT_ADMIN_LOADER_V4_ENTRIES: &[&str] = &["transfer_authority"]; +/// Component contract for `spl_token_surface`. +pub(crate) const MT_ADMIN_SPL_TOKEN_SURFACE: &str = "spl.token"; +/// Component contract for `spl_token_2022_surface`. +pub(crate) const MT_ADMIN_SPL_TOKEN_2022_SURFACE: &str = "spl.token_2022"; +/// Component contract for `spl_elgamal_registry_surface`. +pub(crate) const MT_ADMIN_SPL_ELGAMAL_REGISTRY_SURFACE: &str = "spl.elgamal_registry"; +/// Component contract for `spl_elgamal_registry_entries`. +pub(crate) const MT_ADMIN_SPL_ELGAMAL_REGISTRY_ENTRIES: &[&str] = + &["create_registry", "update_registry"]; +/// Component contract for `spl_token_authority_entries`. +pub(crate) const MT_ADMIN_SPL_TOKEN_AUTHORITY_ENTRIES: &[&str] = &[ + "set_authority", + "initialize_permanent_delegate", + "initialize_interest_bearing_mint", + "update_interest_bearing_rate", +]; +/// Component contract for `spl_token_multisig_entries`. +pub(crate) const MT_ADMIN_SPL_TOKEN_MULTISIG_ENTRIES: &[&str] = + &["initialize_multisig", "initialize_multisig2"]; +/// Component contract for `spl_token_2022_extension_admin_entries`. +pub(crate) const MT_ADMIN_SPL_TOKEN_2022_EXTENSION_ADMIN_ENTRIES: &[&str] = &[ + "initialize_transfer_hook", + "update_transfer_hook_program_id", + "initialize_metadata_pointer", + "update_metadata_pointer", + "initialize_group_pointer", + "update_group_pointer", + "initialize_group_member_pointer", + "update_group_member_pointer", + "initialize_scaled_ui_amount", + "update_scaled_ui_amount_multiplier", +]; diff --git a/kb-lib/src/materializer/admin/core.rs b/kb-lib/src/materializer/admin/materializer.rs similarity index 92% rename from kb-lib/src/materializer/admin/core.rs rename to kb-lib/src/materializer/admin/materializer.rs index 4ccc7c2..1d59a5a 100644 --- a/kb-lib/src/materializer/admin/core.rs +++ b/kb-lib/src/materializer/admin/materializer.rs @@ -1,42 +1,8 @@ -// file: kb-lib/src/materializer/admin/core.rs -// version: 20 +// file: kb-lib/src/materializer/admin/materializer.rs +// version: 22 //! Stable administrative projections derived from exact committed observations. -const ACCEPTED_FAMILIES: &[crate::MdEventFamily] = - &[crate::MdEventFamily::Admin, crate::MdEventFamily::TokenAccount]; -const SYSTEM_SURFACE: &str = "solana.core.system"; -const SYSTEM_ENTRIES: &[&str] = &["assign", "assign_with_seed"]; -const CONFIG_SURFACE: &str = "solana.core.config"; -const CONFIG_ENTRIES: &[&str] = &["store"]; -const BPF_LOADER_UPGRADEABLE_SURFACE: &str = "solana.core.bpf_loader_upgradeable"; -const BPF_LOADER_UPGRADEABLE_ENTRIES: &[&str] = &["set_authority", "set_authority_checked"]; -const LOADER_V4_SURFACE: &str = "solana.core.loader_v4"; -const LOADER_V4_ENTRIES: &[&str] = &["transfer_authority"]; -const SPL_TOKEN_SURFACE: &str = "spl.token"; -const SPL_TOKEN_2022_SURFACE: &str = "spl.token_2022"; -const SPL_ELGAMAL_REGISTRY_SURFACE: &str = "spl.elgamal_registry"; -const SPL_ELGAMAL_REGISTRY_ENTRIES: &[&str] = &["create_registry", "update_registry"]; -const SPL_TOKEN_AUTHORITY_ENTRIES: &[&str] = &[ - "set_authority", - "initialize_permanent_delegate", - "initialize_interest_bearing_mint", - "update_interest_bearing_rate", -]; -const SPL_TOKEN_MULTISIG_ENTRIES: &[&str] = &["initialize_multisig", "initialize_multisig2"]; -const SPL_TOKEN_2022_EXTENSION_ADMIN_ENTRIES: &[&str] = &[ - "initialize_transfer_hook", - "update_transfer_hook_program_id", - "initialize_metadata_pointer", - "update_metadata_pointer", - "initialize_group_pointer", - "update_group_pointer", - "initialize_group_member_pointer", - "update_group_member_pointer", - "initialize_scaled_ui_amount", - "update_scaled_ui_amount_multiplier", -]; - #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum ProjectionKind { SystemAccountOwnership, @@ -91,7 +57,7 @@ pub struct MtAdminMaterializer; impl crate::MtMaterializer for crate::MtAdminMaterializer { fn materializer_name(&self) -> &'static str { - return "kb-lib.materializer.admin"; + return crate::MT_ADMIN_COMPONENT_NAME; } fn materializer_version(&self) -> &'static str { @@ -99,7 +65,7 @@ impl crate::MtMaterializer for crate::MtAdminMaterializer { } fn accepts_event(&self, event: &crate::MdDecodedProtocolEvent) -> bool { - return ACCEPTED_FAMILIES.contains(&event.event_family) + return crate::MT_ADMIN_ACCEPTED_FAMILIES.contains(&event.event_family) && program_matches_surface(event) && projection(event.surface_code.0.as_str(), event.event_name.0.as_str()).is_some(); } @@ -118,17 +84,17 @@ impl crate::MtMaterializer for crate::MtAdminMaterializer { impl crate::MtApiEventMaterializer for crate::MtAdminMaterializer { fn identity(&self) -> crate::MtApiMaterializerIdentity { return crate::MtApiMaterializerIdentity { - name: "materializer.admin".to_string(), + name: crate::MT_ADMIN_PROCESSOR_NAME.to_string(), version: env!("CARGO_PKG_VERSION").to_string(), }; } fn accepted_families(&self) -> &'static [crate::MdEventFamily] { - return ACCEPTED_FAMILIES; + return crate::MT_ADMIN_ACCEPTED_FAMILIES; } fn accepts_observation(&self, observation: &crate::DcApiDecodedObservation) -> bool { - return ACCEPTED_FAMILIES.contains(&observation.event.event_family) + return crate::MT_ADMIN_ACCEPTED_FAMILIES.contains(&observation.event.event_family) && program_matches_surface(&observation.event) && projection( observation.event.surface_code.0.as_str(), @@ -165,7 +131,7 @@ impl crate::MtApiEventMaterializer for crate::MtAdminMaterializer { return crate::MtApiMaterializerExecutionResult::ignored(); }, }; - if !ACCEPTED_FAMILIES.contains(&observation.event.event_family) { + if !crate::MT_ADMIN_ACCEPTED_FAMILIES.contains(&observation.event.event_family) { return crate::MtApiMaterializerExecutionResult::ignored(); } if !program_matches_surface(&observation.event) { @@ -202,7 +168,7 @@ impl crate::MtApiEventMaterializer for crate::MtAdminMaterializer { .output_key(observation.event.surface_code.0.as_str(), operation.as_str()), family: crate::MdMaterializedEventFamily::Admin, payload_json: serde_json::json!({ - "projectionVersion": 1, + "projectionVersion": crate::MT_ADMIN_PROJECTION_VERSION, "projectionSemantics": "committed_instruction_admin_event", "domain": projection.domain(), "loaderSurface": projection.loader_surface(), @@ -212,6 +178,10 @@ impl crate::MtApiEventMaterializer for crate::MtAdminMaterializer { "slot": observation.event.slot.0, "instructionPath": observation.event.instruction_path.0.clone(), "transactionSucceeded": true, + "provenance": { + "processorName": crate::MT_ADMIN_PROCESSOR_NAME, + "processorVersion": env!("CARGO_PKG_VERSION"), + }, "accounts": accounts, "parameters": parameters, "projection": projection_details( @@ -240,41 +210,51 @@ impl crate::MtApiEventMaterializer for crate::MtAdminMaterializer { } fn projection(surface_code: &str, entry_code: &str) -> std::option::Option { - if surface_code == SYSTEM_SURFACE && contains(SYSTEM_ENTRIES, entry_code) { + if surface_code == crate::MT_ADMIN_SYSTEM_SURFACE + && contains(crate::MT_ADMIN_SYSTEM_ENTRIES, entry_code) + { return std::option::Option::Some(ProjectionKind::SystemAccountOwnership); } - if surface_code == CONFIG_SURFACE && contains(CONFIG_ENTRIES, entry_code) { + if surface_code == crate::MT_ADMIN_CONFIG_SURFACE + && contains(crate::MT_ADMIN_CONFIG_ENTRIES, entry_code) + { return std::option::Option::Some(ProjectionKind::ConfigAccountUpdate); } - if surface_code == BPF_LOADER_UPGRADEABLE_SURFACE - && contains(BPF_LOADER_UPGRADEABLE_ENTRIES, entry_code) + if surface_code == crate::MT_ADMIN_BPF_LOADER_UPGRADEABLE_SURFACE + && contains(crate::MT_ADMIN_BPF_LOADER_UPGRADEABLE_ENTRIES, entry_code) { return std::option::Option::Some(ProjectionKind::ProgramLoaderAuthority( - BPF_LOADER_UPGRADEABLE_SURFACE, + crate::MT_ADMIN_BPF_LOADER_UPGRADEABLE_SURFACE, )); } - if surface_code == LOADER_V4_SURFACE && contains(LOADER_V4_ENTRIES, entry_code) { + if surface_code == crate::MT_ADMIN_LOADER_V4_SURFACE + && contains(crate::MT_ADMIN_LOADER_V4_ENTRIES, entry_code) + { return std::option::Option::Some(ProjectionKind::ProgramLoaderAuthority( - LOADER_V4_SURFACE, + crate::MT_ADMIN_LOADER_V4_SURFACE, )); } - if matches!(surface_code, SPL_TOKEN_SURFACE | SPL_TOKEN_2022_SURFACE) - && contains(SPL_TOKEN_AUTHORITY_ENTRIES, entry_code) + if matches!( + surface_code, + crate::MT_ADMIN_SPL_TOKEN_SURFACE | crate::MT_ADMIN_SPL_TOKEN_2022_SURFACE + ) && contains(crate::MT_ADMIN_SPL_TOKEN_AUTHORITY_ENTRIES, entry_code) { return std::option::Option::Some(ProjectionKind::SplTokenAuthority); } - if matches!(surface_code, SPL_TOKEN_SURFACE | SPL_TOKEN_2022_SURFACE) - && contains(SPL_TOKEN_MULTISIG_ENTRIES, entry_code) + if matches!( + surface_code, + crate::MT_ADMIN_SPL_TOKEN_SURFACE | crate::MT_ADMIN_SPL_TOKEN_2022_SURFACE + ) && contains(crate::MT_ADMIN_SPL_TOKEN_MULTISIG_ENTRIES, entry_code) { return std::option::Option::Some(ProjectionKind::SplTokenMultisig); } - if surface_code == SPL_TOKEN_2022_SURFACE - && contains(SPL_TOKEN_2022_EXTENSION_ADMIN_ENTRIES, entry_code) + if surface_code == crate::MT_ADMIN_SPL_TOKEN_2022_SURFACE + && contains(crate::MT_ADMIN_SPL_TOKEN_2022_EXTENSION_ADMIN_ENTRIES, entry_code) { return std::option::Option::Some(ProjectionKind::SplToken2022ExtensionAdmin); } - if surface_code == SPL_ELGAMAL_REGISTRY_SURFACE - && contains(SPL_ELGAMAL_REGISTRY_ENTRIES, entry_code) + if surface_code == crate::MT_ADMIN_SPL_ELGAMAL_REGISTRY_SURFACE + && contains(crate::MT_ADMIN_SPL_ELGAMAL_REGISTRY_ENTRIES, entry_code) { return std::option::Option::Some(ProjectionKind::SplElgamalRegistry); } @@ -282,13 +262,13 @@ fn projection(surface_code: &str, entry_code: &str) -> std::option::Option bool { - if event.surface_code.0 == SPL_TOKEN_SURFACE { + if event.surface_code.0 == crate::MT_ADMIN_SPL_TOKEN_SURFACE { return event.program_id.0 == kb_program_ids::SPL_TOKEN_PROGRAM_ID; } - if event.surface_code.0 == SPL_TOKEN_2022_SURFACE { + if event.surface_code.0 == crate::MT_ADMIN_SPL_TOKEN_2022_SURFACE { return event.program_id.0 == kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID; } - if event.surface_code.0 == SPL_ELGAMAL_REGISTRY_SURFACE { + if event.surface_code.0 == crate::MT_ADMIN_SPL_ELGAMAL_REGISTRY_SURFACE { return event.program_id.0 == kb_program_ids::SPL_TOKEN_2022_ELGAMAL_REGISTRY_PROGRAM_ID; } return true; @@ -537,7 +517,7 @@ mod tests { fn system_assign_creates_owner_projection() { let materializer = crate::MtAdminMaterializer; let observation = observation( - super::SYSTEM_SURFACE, + crate::MT_ADMIN_SYSTEM_SURFACE, "assign", false, serde_json::json!([account("assigned_account", "account111",)]), @@ -554,7 +534,7 @@ mod tests { fn seeded_assign_preserves_base_seed_and_owner() { let materializer = crate::MtAdminMaterializer; let observation = observation( - super::SYSTEM_SURFACE, + crate::MT_ADMIN_SYSTEM_SURFACE, "assign_with_seed", false, serde_json::json!([ @@ -573,7 +553,7 @@ mod tests { fn config_store_preserves_bounded_opaque_audit_fields() { let materializer = crate::MtAdminMaterializer; let observation = observation( - super::CONFIG_SURFACE, + crate::MT_ADMIN_CONFIG_SURFACE, "store", false, serde_json::json!([ @@ -600,13 +580,17 @@ mod tests { fn loader_authority_variants_create_exact_admin_outputs() { let materializer = crate::MtAdminMaterializer; for (surface, entry, target_role) in [ - (super::BPF_LOADER_UPGRADEABLE_SURFACE, "set_authority", "authority_target"), ( - super::BPF_LOADER_UPGRADEABLE_SURFACE, + crate::MT_ADMIN_BPF_LOADER_UPGRADEABLE_SURFACE, + "set_authority", + "authority_target", + ), + ( + crate::MT_ADMIN_BPF_LOADER_UPGRADEABLE_SURFACE, "set_authority_checked", "authority_target", ), - (super::LOADER_V4_SURFACE, "transfer_authority", "program_account"), + (crate::MT_ADMIN_LOADER_V4_SURFACE, "transfer_authority", "program_account"), ] { let observation = observation( surface, @@ -629,7 +613,7 @@ mod tests { #[test] fn classic_token_authority_and_multisig_create_distinct_admin_outputs() { let mut authority = observation( - super::SPL_TOKEN_SURFACE, + crate::MT_ADMIN_SPL_TOKEN_SURFACE, "set_authority", false, serde_json::json!([ @@ -649,7 +633,7 @@ mod tests { ); let mut multisig = observation( - super::SPL_TOKEN_SURFACE, + crate::MT_ADMIN_SPL_TOKEN_SURFACE, "initialize_multisig2", false, serde_json::json!([ @@ -678,7 +662,7 @@ mod tests { #[test] fn token_2022_authority_change_uses_the_admin_owner_once() { let mut observation = observation( - super::SPL_TOKEN_2022_SURFACE, + crate::MT_ADMIN_SPL_TOKEN_2022_SURFACE, "set_authority", false, serde_json::json!([ @@ -703,7 +687,7 @@ mod tests { fn failed_admin_mutation_is_refused() { let materializer = crate::MtAdminMaterializer; let observation = observation( - super::SYSTEM_SURFACE, + crate::MT_ADMIN_SYSTEM_SURFACE, "assign", true, serde_json::json!([account("assigned_account", "account111",)]), @@ -754,7 +738,7 @@ mod tests { fn output_serialization_is_deterministic() { let materializer = crate::MtAdminMaterializer; let observation = observation( - super::CONFIG_SURFACE, + crate::MT_ADMIN_CONFIG_SURFACE, "store", false, serde_json::json!([account("config_account", "config111",)]), @@ -788,7 +772,7 @@ mod tests { ("update_registry", 0_i64, 2_u64, "elgamal_registry_key_updated"), ] { let mut observation = observation( - super::SPL_ELGAMAL_REGISTRY_SURFACE, + crate::MT_ADMIN_SPL_ELGAMAL_REGISTRY_SURFACE, operation, false, serde_json::json!([ @@ -830,7 +814,7 @@ mod tests { fn elgamal_registry_foreign_program_and_failed_transaction_are_refused() { let materializer = crate::MtAdminMaterializer; let mut foreign = observation( - super::SPL_ELGAMAL_REGISTRY_SURFACE, + crate::MT_ADMIN_SPL_ELGAMAL_REGISTRY_SURFACE, "create_registry", false, serde_json::json!([]), @@ -858,7 +842,7 @@ mod tests { ("update_group_member_pointer", "memberAddress", "member111"), ] { let mut observation = observation( - super::SPL_TOKEN_2022_SURFACE, + crate::MT_ADMIN_SPL_TOKEN_2022_SURFACE, operation, false, serde_json::json!([{"position":0,"accountKey":"mint111"}]), @@ -881,7 +865,7 @@ mod tests { #[test] fn token_2022_extension_admin_rejects_foreign_program_and_failed_transaction() { let mut observation = observation( - super::SPL_TOKEN_2022_SURFACE, + crate::MT_ADMIN_SPL_TOKEN_2022_SURFACE, "initialize_metadata_pointer", false, serde_json::json!([]), @@ -937,7 +921,7 @@ pub fn materializer_admin_materialize_token_2022_state_snapshots( output_key: output_key.clone(), family: crate::MdMaterializedEventFamily::Admin, payload_json: serde_json::json!({ - "projectionVersion": 1, + "projectionVersion": crate::MT_ADMIN_PROJECTION_VERSION, "projectionSemantics": "authoritative_bounded_admin_state_snapshot", "domain": "token_2022_extension_admin_state", "idempotenceKey": output_key, @@ -1072,7 +1056,7 @@ pub fn materializer_admin_materialize_elgamal_registry_state_snapshot( output_key: format!("spl_elgamal_registry_state:{registry_account_key}:{slot}"), family: crate::MdMaterializedEventFamily::Admin, payload_json: serde_json::json!({ - "projectionVersion": 1, + "projectionVersion": crate::MT_ADMIN_PROJECTION_VERSION, "domain": "spl_elgamal_registry_account_state", "projectionSemantics": "authoritative_exact_account_snapshot", "idempotenceKey": format!("spl_elgamal_registry:{registry_account_key}:{slot}"), diff --git a/kb-lib/src/materializer/api/contracts.rs b/kb-lib/src/materializer/api/contracts.rs index c6aaf9c..502dab5 100644 --- a/kb-lib/src/materializer/api/contracts.rs +++ b/kb-lib/src/materializer/api/contracts.rs @@ -1,12 +1,12 @@ // file: kb-lib/src/materializer/api/contracts.rs -// version: 7 +// version: 8 //! Backend-neutral decoded observation materialization contracts. /// Stable identity of one decoded event materializer. #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] pub struct MtApiMaterializerIdentity { - /// Stable lower snake case processor name. + /// Stable dotted lower-snake-case processor name without the runtime crate prefix. pub name: std::string::String, /// Semantic or deterministic implementation version. pub version: std::string::String, @@ -177,7 +177,7 @@ impl MtApiMaterializerExecutionResult { /// Stable decoded observation materializer contract used by the common pipeline. pub trait MtApiEventMaterializer: std::marker::Send + std::marker::Sync { - /// Returns the stable materializer identity. + /// Returns the stable persisted processor identity for this materializer. fn identity(&self) -> MtApiMaterializerIdentity; /// Returns every decoded event family explicitly accepted by this materializer. fn accepted_families(&self) -> &'static [crate::MdEventFamily]; diff --git a/kb-lib/src/materializer/api/materializer.rs b/kb-lib/src/materializer/api/materializer.rs index 9150fbd..0293972 100644 --- a/kb-lib/src/materializer/api/materializer.rs +++ b/kb-lib/src/materializer/api/materializer.rs @@ -1,11 +1,11 @@ // file: kb-lib/src/materializer/api/materializer.rs -// version: 2 +// version: 3 -//! Materializer API contract used by business materializer crates. +//! Legacy materializer API contract used by business materializer modules. -/// Common contract implemented by all business materializer crates. +/// Common compatibility contract implemented by all business materializer modules. pub trait MtMaterializer { - /// Returns the stable materializer name. + /// Returns the stable runtime component name with the `kb-lib.materializer.` prefix. fn materializer_name(&self) -> &'static str; /// Returns the materializer version. diff --git a/kb-lib/src/materializer/bridge.rs b/kb-lib/src/materializer/bridge.rs index 2234b44..d37e523 100644 --- a/kb-lib/src/materializer/bridge.rs +++ b/kb-lib/src/materializer/bridge.rs @@ -1,9 +1,17 @@ // file: kb-lib/src/materializer/bridge.rs -// version: 3 +// version: 4 //! `bridge` materializer family. -mod core; +mod constants; +mod materializer; /// Reserved bridge materializer. -pub use self::core::MtBridgeMaterializer; +pub use self::materializer::MtBridgeMaterializer; + +/// Empty accepted-family boundary for the reserved component. +pub(crate) use self::constants::MT_BRIDGE_ACCEPTED_FAMILIES; +/// Stable runtime component name for the `bridge` materializer. +pub(crate) use self::constants::MT_BRIDGE_COMPONENT_NAME; +/// Stable processor name for the `bridge` materializer. +pub(crate) use self::constants::MT_BRIDGE_PROCESSOR_NAME; diff --git a/kb-lib/src/materializer/bridge/constants.rs b/kb-lib/src/materializer/bridge/constants.rs new file mode 100644 index 0000000..097aab2 --- /dev/null +++ b/kb-lib/src/materializer/bridge/constants.rs @@ -0,0 +1,11 @@ +// file: kb-lib/src/materializer/bridge/constants.rs +// version: 1 + +//! Local constants for the bridge materializer. + +/// Stable runtime component name for the bridge materializer. +pub(crate) const MT_BRIDGE_COMPONENT_NAME: &str = "kb-lib.materializer.bridge"; +/// Stable processor name persisted by the replay ledger. +pub(crate) const MT_BRIDGE_PROCESSOR_NAME: &str = "materializer.bridge"; +/// Empty family boundary retained while the materializer is reserved. +pub(crate) const MT_BRIDGE_ACCEPTED_FAMILIES: &[crate::MdEventFamily] = &[]; diff --git a/kb-lib/src/materializer/bridge/core.rs b/kb-lib/src/materializer/bridge/materializer.rs similarity index 72% rename from kb-lib/src/materializer/bridge/core.rs rename to kb-lib/src/materializer/bridge/materializer.rs index de2e2b8..06e0bb7 100644 --- a/kb-lib/src/materializer/bridge/core.rs +++ b/kb-lib/src/materializer/bridge/materializer.rs @@ -1,5 +1,5 @@ -// file: kb-lib/src/materializer/bridge/core.rs -// version: 3 +// file: kb-lib/src/materializer/bridge/materializer.rs +// version: 4 //! Reserved bridge materializer. @@ -9,7 +9,7 @@ pub struct MtBridgeMaterializer; impl crate::MtMaterializer for crate::MtBridgeMaterializer { fn materializer_name(&self) -> &'static str { - return "kb-lib.materializer.bridge"; + return crate::MT_BRIDGE_COMPONENT_NAME; } fn materializer_version(&self) -> &'static str { @@ -22,27 +22,22 @@ impl crate::MtMaterializer for crate::MtBridgeMaterializer { fn materialize_event( &self, - event: &crate::MdDecodedProtocolEvent, + _event: &crate::MdDecodedProtocolEvent, ) -> kb_core::Result> { - return std::result::Result::Ok(std::vec![crate::MdMaterializedEvent { - signature: event.signature.clone(), - slot: event.slot, - decoded_family: event.event_family, - materialized_family: crate::MdMaterializedEventFamily::Bridge, - }]); + return std::result::Result::Ok(std::vec::Vec::new()); } } impl crate::MtApiEventMaterializer for crate::MtBridgeMaterializer { fn identity(&self) -> crate::MtApiMaterializerIdentity { return crate::MtApiMaterializerIdentity { - name: "kb-lib.materializer.bridge".to_string(), + name: crate::MT_BRIDGE_PROCESSOR_NAME.to_string(), version: env!("CARGO_PKG_VERSION").to_string(), }; } fn accepted_families(&self) -> &'static [crate::MdEventFamily] { - return &[]; + return crate::MT_BRIDGE_ACCEPTED_FAMILIES; } fn transaction_policy( diff --git a/kb-lib/src/materializer/compliance.rs b/kb-lib/src/materializer/compliance.rs index 7dd5772..882691e 100644 --- a/kb-lib/src/materializer/compliance.rs +++ b/kb-lib/src/materializer/compliance.rs @@ -1,13 +1,36 @@ // file: kb-lib/src/materializer/compliance.rs -// version: 4 +// version: 5 //! `compliance` materializer family. mod audit; -mod constants; /// Stable native compliance audit materializer. pub use self::audit::MtComplianceAuditMaterializer; +/// Crate-visible `MT_COMPLIANCE_AUDIT_ACCEPTED_FAMILIES` component constant. +pub(crate) use self::audit::MT_COMPLIANCE_AUDIT_ACCEPTED_FAMILIES; +/// Crate-visible `MT_COMPLIANCE_AUDIT_BPF_LOADER_DEPRECATED_SURFACE` component constant. +pub(crate) use self::audit::MT_COMPLIANCE_AUDIT_BPF_LOADER_DEPRECATED_SURFACE; +/// Crate-visible `MT_COMPLIANCE_AUDIT_BPF_LOADER_SURFACE` component constant. +pub(crate) use self::audit::MT_COMPLIANCE_AUDIT_BPF_LOADER_SURFACE; +/// Crate-visible `MT_COMPLIANCE_AUDIT_BPF_LOADER_UPGRADEABLE_SURFACE` component constant. +pub(crate) use self::audit::MT_COMPLIANCE_AUDIT_BPF_LOADER_UPGRADEABLE_SURFACE; +/// Stable runtime component name for the native compliance audit materializer. +pub(crate) use self::audit::MT_COMPLIANCE_AUDIT_COMPONENT_NAME; +/// Crate-visible `MT_COMPLIANCE_AUDIT_COMPUTE_BUDGET_ENTRIES` component constant. +pub(crate) use self::audit::MT_COMPLIANCE_AUDIT_COMPUTE_BUDGET_ENTRIES; +/// Crate-visible `MT_COMPLIANCE_AUDIT_COMPUTE_BUDGET_SURFACE` component constant. +pub(crate) use self::audit::MT_COMPLIANCE_AUDIT_COMPUTE_BUDGET_SURFACE; +/// Crate-visible `MT_COMPLIANCE_AUDIT_LOADER_V4_COPY_ENTRIES` component constant. +pub(crate) use self::audit::MT_COMPLIANCE_AUDIT_LOADER_V4_COPY_ENTRIES; +/// Crate-visible `MT_COMPLIANCE_AUDIT_LOADER_V4_SURFACE` component constant. +pub(crate) use self::audit::MT_COMPLIANCE_AUDIT_LOADER_V4_SURFACE; +/// Stable processor name for the native compliance audit materializer. +pub(crate) use self::audit::MT_COMPLIANCE_AUDIT_PROCESSOR_NAME; +/// Stable projection version for native compliance audit outputs. +pub(crate) use self::audit::MT_COMPLIANCE_AUDIT_PROJECTION_VERSION; +/// Crate-visible `MT_COMPLIANCE_AUDIT_WRITE_ENTRIES` component constant. +pub(crate) use self::audit::MT_COMPLIANCE_AUDIT_WRITE_ENTRIES; /// Canonical tracing target for the native compliance audit materializer. -pub(crate) use self::constants::TRACING_TARGET_MATERIALIZER_COMPLIANCE_AUDIT; +pub(crate) use self::audit::TRACING_TARGET_MATERIALIZER_COMPLIANCE_AUDIT; diff --git a/kb-lib/src/materializer/compliance/audit.rs b/kb-lib/src/materializer/compliance/audit.rs index 997c612..5e5cbf3 100644 --- a/kb-lib/src/materializer/compliance/audit.rs +++ b/kb-lib/src/materializer/compliance/audit.rs @@ -1,577 +1,37 @@ // file: kb-lib/src/materializer/compliance/audit.rs -// version: 10 +// version: 11 -//! Stable compliance-audit projections for committed native bytecode mutations and execution profiles. +//! Native compliance-audit materializer component. -const ACCEPTED_FAMILIES: &[crate::MdEventFamily] = &[crate::MdEventFamily::Audit]; -const COMPUTE_BUDGET_SURFACE: &str = "solana.core.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.core.bpf_loader_deprecated"; -const BPF_LOADER_SURFACE: &str = "solana.core.bpf_loader"; -const BPF_LOADER_UPGRADEABLE_SURFACE: &str = "solana.core.bpf_loader_upgradeable"; -const LOADER_V4_SURFACE: &str = "solana.core.loader_v4"; -const WRITE_ENTRIES: &[&str] = &["write"]; -const LOADER_V4_COPY_ENTRIES: &[&str] = &["copy"]; +mod constants; +mod materializer; -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum ProjectionKind { - ProgramByteWrite(&'static str), - ProgramByteCopy, - ComputeBudgetProfile, -} +/// Stable native compliance audit materializer. +pub use self::materializer::MtComplianceAuditMaterializer; -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 MtComplianceAuditMaterializer; - -impl crate::MtMaterializer for crate::MtComplianceAuditMaterializer { - fn materializer_name(&self) -> &'static str { - return "kb-lib.materializer.compliance.audit"; - } - - fn materializer_version(&self) -> &'static str { - return env!("CARGO_PKG_VERSION"); - } - - fn accepts_event(&self, event: &crate::MdDecodedProtocolEvent) -> bool { - return event.event_family == crate::MdEventFamily::Audit - && projection(event.surface_code.0.as_str(), event.event_name.0.as_str()).is_some(); - } - - fn materialize_event( - &self, - event: &crate::MdDecodedProtocolEvent, - ) -> kb_core::Result> { - if !crate::MtMaterializer::accepts_event(self, event) { - return std::result::Result::Ok(std::vec::Vec::new()); - } - return std::result::Result::Ok(std::vec::Vec::new()); - } -} - -impl crate::MtApiEventMaterializer for crate::MtComplianceAuditMaterializer { - fn identity(&self) -> crate::MtApiMaterializerIdentity { - return crate::MtApiMaterializerIdentity { - name: "materializer.compliance.audit".to_string(), - version: env!("CARGO_PKG_VERSION").to_string(), - }; - } - - fn accepted_families(&self) -> &'static [crate::MdEventFamily] { - return ACCEPTED_FAMILIES; - } - - fn accepts_observation(&self, observation: &crate::DcApiDecodedObservation) -> bool { - return observation.event.event_family == crate::MdEventFamily::Audit - && projection( - observation.event.surface_code.0.as_str(), - observation.event.event_name.0.as_str(), - ) - .is_some(); - } - - fn transaction_policy( - &self, - _family: crate::MdEventFamily, - ) -> crate::MtApiMaterializationTransactionPolicy { - return crate::MtApiMaterializationTransactionPolicy::SuccessfulOrFailedAudit; - } - - fn materialize( - &self, - observation: &crate::DcApiDecodedObservation, - ) -> crate::MtApiMaterializerExecutionResult { - 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::TRACING_TARGET_MATERIALIZER_COMPLIANCE_AUDIT, - 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::MtApiMaterializerExecutionResult::ignored(); - }, - }; - if observation.event.event_family != crate::MdEventFamily::Audit { - return crate::MtApiMaterializerExecutionResult::ignored(); - } - if projection != ProjectionKind::ComputeBudgetProfile - && (observation.transaction_failed || !observation.observation_committed) - { - tracing::debug!( - target: crate::TRACING_TARGET_MATERIALIZER_COMPLIANCE_AUDIT, - 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::MtApiMaterializerExecutionResult::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::MtApiMaterializedOutput { - output_key: projection - .output_key(observation.event.surface_code.0.as_str(), operation.as_str()), - family: crate::MdMaterializedEventFamily::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, ¶meters), - }), - }; - tracing::debug!( - target: crate::TRACING_TARGET_MATERIALIZER_COMPLIANCE_AUDIT, - 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::MtApiMaterializerExecutionResult { - status: crate::MtApiMaterializerOutcomeStatus::Inserted, - outputs: std::vec![output], - diagnostics: std::vec::Vec::new(), - }; - } -} - -fn materialize_compute_budget_profile( - observation: &crate::DcApiDecodedObservation, - parameters: serde_json::Value, - operation: std::string::String, -) -> crate::MtApiMaterializerExecutionResult { - let profile = parameters.get("transactionProfile").cloned().unwrap_or(serde_json::Value::Null); - if !compute_budget_profile_emits(&profile) { - return crate::MtApiMaterializerExecutionResult::ignored(); - } - let output = crate::MtApiMaterializedOutput { - output_key: "transaction_compute_budget_profile:0".to_string(), - family: crate::MdMaterializedEventFamily::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::TRACING_TARGET_MATERIALIZER_COMPLIANCE_AUDIT, - 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::MtApiMaterializerExecutionResult { - status: crate::MtApiMaterializerOutcomeStatus::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 { - 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::DcApiDecodedObservation { - return crate::DcApiDecodedObservation { - event_key: format!("{entry_code}:0"), - event: crate::MdDecodedProtocolEvent { - signature: crate::MdSignature("signature".to_string()), - slot: crate::MdSlot(42), - instruction_path: crate::MdInstructionPath("0".to_string()), - program_id: crate::MdProgramId("program-id".to_string()), - protocol_code: crate::MdProtocolCode("solana.core".to_string()), - surface_code: crate::MdSurfaceCode(surface_code.to_string()), - event_code: crate::MdEventCode(format!("{surface_code}.{entry_code}")), - event_name: crate::MdEventName(entry_code.to_string()), - event_family: crate::MdEventFamily::Audit, - source_kind: crate::MdEventSourceKind::Instruction, - confidence: crate::MdDecoderConfidence::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::DcApiDecoderProof { - kind: crate::DcApiDecoderProofKind::Manual, - confidence: crate::MdDecoderConfidence::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::MtComplianceAuditMaterializer; - 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::MtApiEventMaterializer::materialize(&materializer, &observation); - assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted); - assert_eq!(result.outputs[0].family, crate::MdMaterializedEventFamily::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::MtComplianceAuditMaterializer; - 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::MtApiEventMaterializer::materialize(&materializer, &observation); - assert_eq!( - result.outputs[0].output_key, - "program_loader_code:solana.core.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::MtComplianceAuditMaterializer; - 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::MtApiEventMaterializer::materialize(&materializer, &emitter); - assert_eq!(emitted.status, crate::MtApiMaterializerOutcomeStatus::Inserted); - assert_eq!(emitted.outputs[0].output_key, "transaction_compute_budget_profile:0"); - assert_eq!(emitted.outputs[0].family, crate::MdMaterializedEventFamily::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::MtApiEventMaterializer::materialize(&materializer, &non_emitter); - assert_eq!(ignored.status, crate::MtApiMaterializerOutcomeStatus::Ignored); - } - - #[test] - fn failed_compute_budget_profile_remains_audit_materializable() { - let materializer = crate::MtComplianceAuditMaterializer; - 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::MtApiEventMaterializer::materialize(&materializer, &observation); - assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted); - assert_eq!(result.outputs[0].payload_json["transactionSucceeded"], false); - } - - #[test] - fn failed_code_mutation_is_refused() { - let materializer = crate::MtComplianceAuditMaterializer; - 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::MtApiEventMaterializer::materialize(&materializer, &observation); - assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Refused); - assert!(result.outputs.is_empty()); - } - - #[test] - fn unrelated_audit_observation_is_ignored() { - let materializer = crate::MtComplianceAuditMaterializer; - let observation = observation( - "other_surface", - "write", - false, - serde_json::json!([]), - serde_json::json!({}), - ); - let result = crate::MtApiEventMaterializer::materialize(&materializer, &observation); - assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Ignored); - } - - #[test] - fn output_serialization_is_deterministic() { - let materializer = crate::MtComplianceAuditMaterializer; - 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::MtApiEventMaterializer::materialize(&materializer, &observation); - let second = crate::MtApiEventMaterializer::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); - } -} +/// Crate-visible `MT_COMPLIANCE_AUDIT_ACCEPTED_FAMILIES` component constant. +pub(crate) use self::constants::MT_COMPLIANCE_AUDIT_ACCEPTED_FAMILIES; +/// Crate-visible `MT_COMPLIANCE_AUDIT_BPF_LOADER_DEPRECATED_SURFACE` component constant. +pub(crate) use self::constants::MT_COMPLIANCE_AUDIT_BPF_LOADER_DEPRECATED_SURFACE; +/// Crate-visible `MT_COMPLIANCE_AUDIT_BPF_LOADER_SURFACE` component constant. +pub(crate) use self::constants::MT_COMPLIANCE_AUDIT_BPF_LOADER_SURFACE; +/// Crate-visible `MT_COMPLIANCE_AUDIT_BPF_LOADER_UPGRADEABLE_SURFACE` component constant. +pub(crate) use self::constants::MT_COMPLIANCE_AUDIT_BPF_LOADER_UPGRADEABLE_SURFACE; +/// Stable runtime component name for the native compliance audit materializer. +pub(crate) use self::constants::MT_COMPLIANCE_AUDIT_COMPONENT_NAME; +/// Crate-visible `MT_COMPLIANCE_AUDIT_COMPUTE_BUDGET_ENTRIES` component constant. +pub(crate) use self::constants::MT_COMPLIANCE_AUDIT_COMPUTE_BUDGET_ENTRIES; +/// Crate-visible `MT_COMPLIANCE_AUDIT_COMPUTE_BUDGET_SURFACE` component constant. +pub(crate) use self::constants::MT_COMPLIANCE_AUDIT_COMPUTE_BUDGET_SURFACE; +/// Crate-visible `MT_COMPLIANCE_AUDIT_LOADER_V4_COPY_ENTRIES` component constant. +pub(crate) use self::constants::MT_COMPLIANCE_AUDIT_LOADER_V4_COPY_ENTRIES; +/// Crate-visible `MT_COMPLIANCE_AUDIT_LOADER_V4_SURFACE` component constant. +pub(crate) use self::constants::MT_COMPLIANCE_AUDIT_LOADER_V4_SURFACE; +/// Stable processor name for the native compliance audit materializer. +pub(crate) use self::constants::MT_COMPLIANCE_AUDIT_PROCESSOR_NAME; +/// Stable projection version for native compliance audit outputs. +pub(crate) use self::constants::MT_COMPLIANCE_AUDIT_PROJECTION_VERSION; +/// Crate-visible `MT_COMPLIANCE_AUDIT_WRITE_ENTRIES` component constant. +pub(crate) use self::constants::MT_COMPLIANCE_AUDIT_WRITE_ENTRIES; +/// Canonical tracing target for the native compliance audit materializer. +pub(crate) use self::constants::TRACING_TARGET_MATERIALIZER_COMPLIANCE_AUDIT; diff --git a/kb-lib/src/materializer/compliance/audit/constants.rs b/kb-lib/src/materializer/compliance/audit/constants.rs new file mode 100644 index 0000000..0815c03 --- /dev/null +++ b/kb-lib/src/materializer/compliance/audit/constants.rs @@ -0,0 +1,42 @@ +// file: kb-lib/src/materializer/compliance/audit/constants.rs +// version: 4 + +//! Local constants for the compliance audit materializer. + +/// Stable runtime component name for the compliance audit materializer. +pub(crate) const MT_COMPLIANCE_AUDIT_COMPONENT_NAME: &str = "kb-lib.materializer.compliance.audit"; +/// Stable processor name persisted by the replay ledger. +pub(crate) const MT_COMPLIANCE_AUDIT_PROCESSOR_NAME: &str = "materializer.compliance.audit"; +/// Stable projection contract version for the compliance audit materializer. +pub(crate) const MT_COMPLIANCE_AUDIT_PROJECTION_VERSION: u32 = 2; +/// Canonical tracing target for the compliance audit materializer. +pub(crate) const TRACING_TARGET_MATERIALIZER_COMPLIANCE_AUDIT: &str = + "kb-lib.materializer.compliance.audit"; + +/// Component contract for `accepted_families`. +pub(crate) const MT_COMPLIANCE_AUDIT_ACCEPTED_FAMILIES: &[crate::MdEventFamily] = + &[crate::MdEventFamily::Audit]; +/// Component contract for `compute_budget_surface`. +pub(crate) const MT_COMPLIANCE_AUDIT_COMPUTE_BUDGET_SURFACE: &str = "solana.core.compute_budget"; +/// Component contract for `compute_budget_entries`. +pub(crate) const MT_COMPLIANCE_AUDIT_COMPUTE_BUDGET_ENTRIES: &[&str] = &[ + "request_units_deprecated", + "request_heap_frame", + "set_compute_unit_limit", + "set_compute_unit_price", + "set_loaded_accounts_data_size_limit", +]; +/// Component contract for `bpf_loader_deprecated_surface`. +pub(crate) const MT_COMPLIANCE_AUDIT_BPF_LOADER_DEPRECATED_SURFACE: &str = + "solana.core.bpf_loader_deprecated"; +/// Component contract for `bpf_loader_surface`. +pub(crate) const MT_COMPLIANCE_AUDIT_BPF_LOADER_SURFACE: &str = "solana.core.bpf_loader"; +/// Component contract for `bpf_loader_upgradeable_surface`. +pub(crate) const MT_COMPLIANCE_AUDIT_BPF_LOADER_UPGRADEABLE_SURFACE: &str = + "solana.core.bpf_loader_upgradeable"; +/// Component contract for `loader_v4_surface`. +pub(crate) const MT_COMPLIANCE_AUDIT_LOADER_V4_SURFACE: &str = "solana.core.loader_v4"; +/// Component contract for `write_entries`. +pub(crate) const MT_COMPLIANCE_AUDIT_WRITE_ENTRIES: &[&str] = &["write"]; +/// Component contract for `loader_v4_copy_entries`. +pub(crate) const MT_COMPLIANCE_AUDIT_LOADER_V4_COPY_ENTRIES: &[&str] = &["copy"]; diff --git a/kb-lib/src/materializer/compliance/audit/materializer.rs b/kb-lib/src/materializer/compliance/audit/materializer.rs new file mode 100644 index 0000000..fe39869 --- /dev/null +++ b/kb-lib/src/materializer/compliance/audit/materializer.rs @@ -0,0 +1,581 @@ +// file: kb-lib/src/materializer/compliance/audit/materializer.rs +// version: 12 + +//! Stable compliance-audit projections for committed native bytecode mutations and execution profiles. + +#[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 => crate::MT_COMPLIANCE_AUDIT_LOADER_V4_SURFACE, + Self::ComputeBudgetProfile => crate::MT_COMPLIANCE_AUDIT_COMPUTE_BUDGET_SURFACE, + }; + } +} + +/// Stable compliance-audit materializer for native bytecode mutations and execution profiles. +#[derive(Clone, Debug, Default)] +pub struct MtComplianceAuditMaterializer; + +impl crate::MtMaterializer for crate::MtComplianceAuditMaterializer { + fn materializer_name(&self) -> &'static str { + return crate::MT_COMPLIANCE_AUDIT_COMPONENT_NAME; + } + + fn materializer_version(&self) -> &'static str { + return env!("CARGO_PKG_VERSION"); + } + + fn accepts_event(&self, event: &crate::MdDecodedProtocolEvent) -> bool { + return event.event_family == crate::MdEventFamily::Audit + && projection(event.surface_code.0.as_str(), event.event_name.0.as_str()).is_some(); + } + + fn materialize_event( + &self, + event: &crate::MdDecodedProtocolEvent, + ) -> kb_core::Result> { + if !crate::MtMaterializer::accepts_event(self, event) { + return std::result::Result::Ok(std::vec::Vec::new()); + } + return std::result::Result::Ok(std::vec::Vec::new()); + } +} + +impl crate::MtApiEventMaterializer for crate::MtComplianceAuditMaterializer { + fn identity(&self) -> crate::MtApiMaterializerIdentity { + return crate::MtApiMaterializerIdentity { + name: crate::MT_COMPLIANCE_AUDIT_PROCESSOR_NAME.to_string(), + version: env!("CARGO_PKG_VERSION").to_string(), + }; + } + + fn accepted_families(&self) -> &'static [crate::MdEventFamily] { + return crate::MT_COMPLIANCE_AUDIT_ACCEPTED_FAMILIES; + } + + fn accepts_observation(&self, observation: &crate::DcApiDecodedObservation) -> bool { + return observation.event.event_family == crate::MdEventFamily::Audit + && projection( + observation.event.surface_code.0.as_str(), + observation.event.event_name.0.as_str(), + ) + .is_some(); + } + + fn transaction_policy( + &self, + _family: crate::MdEventFamily, + ) -> crate::MtApiMaterializationTransactionPolicy { + return crate::MtApiMaterializationTransactionPolicy::SuccessfulOrFailedAudit; + } + + fn materialize( + &self, + observation: &crate::DcApiDecodedObservation, + ) -> crate::MtApiMaterializerExecutionResult { + 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::TRACING_TARGET_MATERIALIZER_COMPLIANCE_AUDIT, + 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::MtApiMaterializerExecutionResult::ignored(); + }, + }; + if observation.event.event_family != crate::MdEventFamily::Audit { + return crate::MtApiMaterializerExecutionResult::ignored(); + } + if projection != ProjectionKind::ComputeBudgetProfile + && (observation.transaction_failed || !observation.observation_committed) + { + tracing::debug!( + target: crate::TRACING_TARGET_MATERIALIZER_COMPLIANCE_AUDIT, + 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::MtApiMaterializerExecutionResult::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::MtApiMaterializedOutput { + output_key: projection + .output_key(observation.event.surface_code.0.as_str(), operation.as_str()), + family: crate::MdMaterializedEventFamily::ComplianceAudit, + payload_json: serde_json::json!({ + "projectionVersion": crate::MT_COMPLIANCE_AUDIT_PROJECTION_VERSION, + "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, + "provenance": { + "processorName": crate::MT_COMPLIANCE_AUDIT_PROCESSOR_NAME, + "processorVersion": env!("CARGO_PKG_VERSION"), + }, + "accounts": accounts, + "parameters": parameters, + "projection": projection_details(projection, &accounts, ¶meters), + }), + }; + tracing::debug!( + target: crate::TRACING_TARGET_MATERIALIZER_COMPLIANCE_AUDIT, + 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::MtApiMaterializerExecutionResult { + status: crate::MtApiMaterializerOutcomeStatus::Inserted, + outputs: std::vec![output], + diagnostics: std::vec::Vec::new(), + }; + } +} + +fn materialize_compute_budget_profile( + observation: &crate::DcApiDecodedObservation, + parameters: serde_json::Value, + operation: std::string::String, +) -> crate::MtApiMaterializerExecutionResult { + let profile = parameters.get("transactionProfile").cloned().unwrap_or(serde_json::Value::Null); + if !compute_budget_profile_emits(&profile) { + return crate::MtApiMaterializerExecutionResult::ignored(); + } + let output = crate::MtApiMaterializedOutput { + output_key: "transaction_compute_budget_profile:0".to_string(), + family: crate::MdMaterializedEventFamily::ComplianceAudit, + payload_json: serde_json::json!({ + "projectionVersion": crate::MT_COMPLIANCE_AUDIT_PROJECTION_VERSION, + "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, + "provenance": { + "processorName": crate::MT_COMPLIANCE_AUDIT_PROCESSOR_NAME, + "processorVersion": env!("CARGO_PKG_VERSION"), + }, + "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::TRACING_TARGET_MATERIALIZER_COMPLIANCE_AUDIT, + 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::MtApiMaterializerExecutionResult { + status: crate::MtApiMaterializerOutcomeStatus::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 { + if surface_code == crate::MT_COMPLIANCE_AUDIT_COMPUTE_BUDGET_SURFACE + && contains(crate::MT_COMPLIANCE_AUDIT_COMPUTE_BUDGET_ENTRIES, entry_code) + { + return std::option::Option::Some(ProjectionKind::ComputeBudgetProfile); + } + if contains(crate::MT_COMPLIANCE_AUDIT_WRITE_ENTRIES, entry_code) { + for supported in [ + crate::MT_COMPLIANCE_AUDIT_BPF_LOADER_DEPRECATED_SURFACE, + crate::MT_COMPLIANCE_AUDIT_BPF_LOADER_SURFACE, + crate::MT_COMPLIANCE_AUDIT_BPF_LOADER_UPGRADEABLE_SURFACE, + crate::MT_COMPLIANCE_AUDIT_LOADER_V4_SURFACE, + ] { + if surface_code == supported { + return std::option::Option::Some(ProjectionKind::ProgramByteWrite(supported)); + } + } + } + if surface_code == crate::MT_COMPLIANCE_AUDIT_LOADER_V4_SURFACE + && contains(crate::MT_COMPLIANCE_AUDIT_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::DcApiDecodedObservation { + return crate::DcApiDecodedObservation { + event_key: format!("{entry_code}:0"), + event: crate::MdDecodedProtocolEvent { + signature: crate::MdSignature("signature".to_string()), + slot: crate::MdSlot(42), + instruction_path: crate::MdInstructionPath("0".to_string()), + program_id: crate::MdProgramId("program-id".to_string()), + protocol_code: crate::MdProtocolCode("solana.core".to_string()), + surface_code: crate::MdSurfaceCode(surface_code.to_string()), + event_code: crate::MdEventCode(format!("{surface_code}.{entry_code}")), + event_name: crate::MdEventName(entry_code.to_string()), + event_family: crate::MdEventFamily::Audit, + source_kind: crate::MdEventSourceKind::Instruction, + confidence: crate::MdDecoderConfidence::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::DcApiDecoderProof { + kind: crate::DcApiDecoderProofKind::Manual, + confidence: crate::MdDecoderConfidence::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::MtComplianceAuditMaterializer; + for (surface, target_role, authority_role) in [ + ( + crate::MT_COMPLIANCE_AUDIT_BPF_LOADER_DEPRECATED_SURFACE, + "program_account", + "missing", + ), + (crate::MT_COMPLIANCE_AUDIT_BPF_LOADER_SURFACE, "program_account", "missing"), + ( + crate::MT_COMPLIANCE_AUDIT_BPF_LOADER_UPGRADEABLE_SURFACE, + "buffer_account", + "buffer_authority", + ), + (crate::MT_COMPLIANCE_AUDIT_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::MtApiEventMaterializer::materialize(&materializer, &observation); + assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted); + assert_eq!(result.outputs[0].family, crate::MdMaterializedEventFamily::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::MtComplianceAuditMaterializer; + let observation = observation( + crate::MT_COMPLIANCE_AUDIT_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::MtApiEventMaterializer::materialize(&materializer, &observation); + assert_eq!( + result.outputs[0].output_key, + "program_loader_code:solana.core.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::MtComplianceAuditMaterializer; + let emitter = observation( + crate::MT_COMPLIANCE_AUDIT_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::MtApiEventMaterializer::materialize(&materializer, &emitter); + assert_eq!(emitted.status, crate::MtApiMaterializerOutcomeStatus::Inserted); + assert_eq!(emitted.outputs[0].output_key, "transaction_compute_budget_profile:0"); + assert_eq!(emitted.outputs[0].family, crate::MdMaterializedEventFamily::ComplianceAudit); + assert_eq!(emitted.outputs[0].payload_json["profile"]["computeBudgetInstructionCount"], 2); + let non_emitter = observation( + crate::MT_COMPLIANCE_AUDIT_COMPUTE_BUDGET_SURFACE, + "set_compute_unit_price", + false, + serde_json::json!([]), + serde_json::json!({ + "transactionProfile": { + "currentInstructionIsProfileEmitter": false, + "profileEmitterInstructionPath": "0" + } + }), + ); + let ignored = crate::MtApiEventMaterializer::materialize(&materializer, &non_emitter); + assert_eq!(ignored.status, crate::MtApiMaterializerOutcomeStatus::Ignored); + } + + #[test] + fn failed_compute_budget_profile_remains_audit_materializable() { + let materializer = crate::MtComplianceAuditMaterializer; + let observation = observation( + crate::MT_COMPLIANCE_AUDIT_COMPUTE_BUDGET_SURFACE, + "set_compute_unit_price", + true, + serde_json::json!([]), + serde_json::json!({ + "transactionProfile": { + "currentInstructionIsProfileEmitter": true, + "profileEmitterInstructionPath": "0", + "computeBudgetInstructionCount": 1 + } + }), + ); + let result = crate::MtApiEventMaterializer::materialize(&materializer, &observation); + assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted); + assert_eq!(result.outputs[0].payload_json["transactionSucceeded"], false); + } + + #[test] + fn failed_code_mutation_is_refused() { + let materializer = crate::MtComplianceAuditMaterializer; + let observation = observation( + crate::MT_COMPLIANCE_AUDIT_LOADER_V4_SURFACE, + "write", + true, + serde_json::json!([account("program_account", "target111")]), + serde_json::json!({"offset": 0, "byteLength": 1, "bytesSha256": "abc"}), + ); + let result = crate::MtApiEventMaterializer::materialize(&materializer, &observation); + assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Refused); + assert!(result.outputs.is_empty()); + } + + #[test] + fn unrelated_audit_observation_is_ignored() { + let materializer = crate::MtComplianceAuditMaterializer; + let observation = observation( + "other_surface", + "write", + false, + serde_json::json!([]), + serde_json::json!({}), + ); + let result = crate::MtApiEventMaterializer::materialize(&materializer, &observation); + assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Ignored); + } + + #[test] + fn output_serialization_is_deterministic() { + let materializer = crate::MtComplianceAuditMaterializer; + let observation = observation( + crate::MT_COMPLIANCE_AUDIT_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::MtApiEventMaterializer::materialize(&materializer, &observation); + let second = crate::MtApiEventMaterializer::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); + } +} diff --git a/kb-lib/src/materializer/compliance/constants.rs b/kb-lib/src/materializer/compliance/constants.rs deleted file mode 100644 index d24998e..0000000 --- a/kb-lib/src/materializer/compliance/constants.rs +++ /dev/null @@ -1,8 +0,0 @@ -// file: kb-lib/src/materializer/compliance/constants.rs -// version: 2 - -//! Local constants for the native compliance audit materializer. - -/// Canonical tracing target for this component. -pub(crate) const TRACING_TARGET_MATERIALIZER_COMPLIANCE_AUDIT: &str = - "kb-lib.materializer.compliance"; diff --git a/kb-lib/src/materializer/fees.rs b/kb-lib/src/materializer/fees.rs index a2f1ac4..2578a3d 100644 --- a/kb-lib/src/materializer/fees.rs +++ b/kb-lib/src/materializer/fees.rs @@ -1,11 +1,27 @@ // file: kb-lib/src/materializer/fees.rs -// version: 4 +// version: 5 //! `fees` materializer family. -mod core; +mod constants; +mod materializer; /// Fee materializer for committed Token-2022 fee observations. -pub use self::core::MtFeesMaterializer; +pub use self::materializer::MtFeesMaterializer; /// Materializes authoritative Token-2022 fee extension snapshots. -pub use self::core::materializer_fees_materialize_token_2022_state_snapshots; +pub use self::materializer::materializer_fees_materialize_token_2022_state_snapshots; + +/// Crate-visible `MT_FEES_ACCEPTED_FAMILIES` component constant. +pub(crate) use self::constants::MT_FEES_ACCEPTED_FAMILIES; +/// Stable runtime component name for the `fees` materializer. +pub(crate) use self::constants::MT_FEES_COMPONENT_NAME; +/// Stable processor name for the `fees` materializer. +pub(crate) use self::constants::MT_FEES_PROCESSOR_NAME; +/// Stable projection version for the `fees` materializer. +pub(crate) use self::constants::MT_FEES_PROJECTION_VERSION; +/// Crate-visible `MT_FEES_SPL_TOKEN_2022_SURFACE` component constant. +pub(crate) use self::constants::MT_FEES_SPL_TOKEN_2022_SURFACE; +/// Crate-visible `MT_FEES_TOKEN_2022_FEE_ENTRIES` component constant. +pub(crate) use self::constants::MT_FEES_TOKEN_2022_FEE_ENTRIES; +/// Canonical tracing target for the `fees` materializer. +pub(crate) use self::constants::TRACING_TARGET_MATERIALIZER_FEES; diff --git a/kb-lib/src/materializer/fees/constants.rs b/kb-lib/src/materializer/fees/constants.rs new file mode 100644 index 0000000..11dfc74 --- /dev/null +++ b/kb-lib/src/materializer/fees/constants.rs @@ -0,0 +1,34 @@ +// file: kb-lib/src/materializer/fees/constants.rs +// version: 2 + +//! Local constants for the fees materializer. + +/// Stable runtime component name for the fees materializer. +pub(crate) const MT_FEES_COMPONENT_NAME: &str = "kb-lib.materializer.fees"; +/// Stable processor name persisted by the replay ledger. +pub(crate) const MT_FEES_PROCESSOR_NAME: &str = "materializer.fees"; +/// Stable projection contract version for the fees materializer. +pub(crate) const MT_FEES_PROJECTION_VERSION: u32 = 2; +/// Canonical tracing target for the fees materializer. +pub(crate) const TRACING_TARGET_MATERIALIZER_FEES: &str = "kb-lib.materializer.fees"; + +/// Component contract for `accepted_families`. +pub(crate) const MT_FEES_ACCEPTED_FAMILIES: &[crate::MdEventFamily] = &[crate::MdEventFamily::Fee]; +/// Component contract for `spl_token_2022_surface`. +pub(crate) const MT_FEES_SPL_TOKEN_2022_SURFACE: &str = "spl.token_2022"; +/// Component contract for `token_2022_fee_entries`. +pub(crate) const MT_FEES_TOKEN_2022_FEE_ENTRIES: &[&str] = &[ + "initialize_transfer_fee_config", + "set_transfer_fee", + "withdraw_withheld_tokens_from_mint", + "withdraw_withheld_tokens_from_accounts", + "harvest_withheld_tokens_to_mint", + "transfer_checked_with_fee", + "initialize_confidential_transfer_fee_config", + "withdraw_confidential_withheld_tokens_from_mint", + "withdraw_confidential_withheld_tokens_from_accounts", + "harvest_confidential_withheld_tokens_to_mint", + "enable_confidential_harvest_to_mint", + "disable_confidential_harvest_to_mint", + "transfer_confidential_tokens_with_fee", +]; diff --git a/kb-lib/src/materializer/fees/core.rs b/kb-lib/src/materializer/fees/materializer.rs similarity index 81% rename from kb-lib/src/materializer/fees/core.rs rename to kb-lib/src/materializer/fees/materializer.rs index dfab844..ec5d523 100644 --- a/kb-lib/src/materializer/fees/core.rs +++ b/kb-lib/src/materializer/fees/materializer.rs @@ -1,33 +1,15 @@ -// file: kb-lib/src/materializer/fees/core.rs -// version: 5 +// file: kb-lib/src/materializer/fees/materializer.rs +// version: 8 //! Stable fee projections derived from exact committed Token-2022 observations. -const ACCEPTED_FAMILIES: &[crate::MdEventFamily] = &[crate::MdEventFamily::Fee]; -const SPL_TOKEN_2022_SURFACE: &str = "spl.token_2022"; -const TOKEN_2022_FEE_ENTRIES: &[&str] = &[ - "initialize_transfer_fee_config", - "set_transfer_fee", - "withdraw_withheld_tokens_from_mint", - "withdraw_withheld_tokens_from_accounts", - "harvest_withheld_tokens_to_mint", - "transfer_checked_with_fee", - "initialize_confidential_transfer_fee_config", - "withdraw_confidential_withheld_tokens_from_mint", - "withdraw_confidential_withheld_tokens_from_accounts", - "harvest_confidential_withheld_tokens_to_mint", - "enable_confidential_harvest_to_mint", - "disable_confidential_harvest_to_mint", - "transfer_confidential_tokens_with_fee", -]; - /// Fee materializer for committed Token-2022 fee instructions. #[derive(Clone, Debug, Default)] pub struct MtFeesMaterializer; impl crate::MtMaterializer for crate::MtFeesMaterializer { fn materializer_name(&self) -> &'static str { - return "kb-lib.materializer.fees"; + return crate::MT_FEES_COMPONENT_NAME; } fn materializer_version(&self) -> &'static str { return env!("CARGO_PKG_VERSION"); @@ -46,12 +28,12 @@ impl crate::MtMaterializer for crate::MtFeesMaterializer { impl crate::MtApiEventMaterializer for crate::MtFeesMaterializer { fn identity(&self) -> crate::MtApiMaterializerIdentity { return crate::MtApiMaterializerIdentity { - name: "materializer.fees".to_string(), + name: crate::MT_FEES_PROCESSOR_NAME.to_string(), version: env!("CARGO_PKG_VERSION").to_string(), }; } fn accepted_families(&self) -> &'static [crate::MdEventFamily] { - return ACCEPTED_FAMILIES; + return crate::MT_FEES_ACCEPTED_FAMILIES; } fn accepts_observation(&self, observation: &crate::DcApiDecodedObservation) -> bool { return accepts_event(&observation.event); @@ -67,9 +49,35 @@ impl crate::MtApiEventMaterializer for crate::MtFeesMaterializer { observation: &crate::DcApiDecodedObservation, ) -> crate::MtApiMaterializerExecutionResult { if !accepts_event(&observation.event) { + tracing::debug!( + target: crate::TRACING_TARGET_MATERIALIZER_FEES, + action = "materialize_fee_observation", + component_name = crate::MT_FEES_COMPONENT_NAME, + processor_name = crate::MT_FEES_PROCESSOR_NAME, + processor_version = env!("CARGO_PKG_VERSION"), + surface_code = %observation.event.surface_code.0.as_str(), + entry_code = %observation.event.event_name.0.as_str(), + accepted = false, + status = "ignored", + "ignore observation outside supported Token-2022 fee projections" + ); return crate::MtApiMaterializerExecutionResult::ignored(); } if observation.transaction_failed || !observation.observation_committed { + tracing::debug!( + target: crate::TRACING_TARGET_MATERIALIZER_FEES, + action = "materialize_fee_observation", + component_name = crate::MT_FEES_COMPONENT_NAME, + processor_name = crate::MT_FEES_PROCESSOR_NAME, + processor_version = env!("CARGO_PKG_VERSION"), + 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, + status = "refused", + error_code = "failed_transaction_fee_refused", + "refuse failed or uncommitted Token-2022 fee observation" + ); return crate::MtApiMaterializerExecutionResult::refused( "failed_transaction_fee_refused", "failed or uncommitted Token-2022 observations cannot create fee outputs", @@ -90,7 +98,7 @@ impl crate::MtApiEventMaterializer for crate::MtFeesMaterializer { output_key: format!("token_2022_fee:{operation}:0"), family: crate::MdMaterializedEventFamily::Fee, payload_json: serde_json::json!({ - "projectionVersion": 1, + "projectionVersion": crate::MT_FEES_PROJECTION_VERSION, "projectionSemantics": "committed_instruction_fee_event", "domain": fee_domain(observation.event.event_name.0.as_str()), "operation": operation, @@ -99,6 +107,10 @@ impl crate::MtApiEventMaterializer for crate::MtFeesMaterializer { "slot": observation.event.slot.0, "instructionPath": observation.event.instruction_path.0.clone(), "transactionSucceeded": true, + "provenance": { + "processorName": crate::MT_FEES_PROCESSOR_NAME, + "processorVersion": env!("CARGO_PKG_VERSION"), + }, "accounts": accounts, "parameters": parameters, "stateReadRequired": state_read_required( @@ -107,6 +119,21 @@ impl crate::MtApiEventMaterializer for crate::MtFeesMaterializer { "confidentialValuesDecrypted": false, }), }; + tracing::debug!( + target: crate::TRACING_TARGET_MATERIALIZER_FEES, + action = "materialize_fee_observation", + component_name = crate::MT_FEES_COMPONENT_NAME, + processor_name = crate::MT_FEES_PROCESSOR_NAME, + processor_version = env!("CARGO_PKG_VERSION"), + surface_code = %observation.event.surface_code.0.as_str(), + entry_code = %observation.event.event_name.0.as_str(), + signature = %observation.event.signature.0.as_str(), + slot = observation.event.slot.0, + instruction_path = %observation.event.instruction_path.0.as_str(), + output_count = 1_usize, + status = "inserted", + "materialize committed Token-2022 fee observation" + ); return crate::MtApiMaterializerExecutionResult { status: crate::MtApiMaterializerOutcomeStatus::Inserted, outputs: std::vec![output], @@ -117,9 +144,9 @@ impl crate::MtApiEventMaterializer for crate::MtFeesMaterializer { fn accepts_event(event: &crate::MdDecodedProtocolEvent) -> bool { return event.event_family == crate::MdEventFamily::Fee - && event.surface_code.0 == SPL_TOKEN_2022_SURFACE + && event.surface_code.0 == crate::MT_FEES_SPL_TOKEN_2022_SURFACE && event.program_id.0 == kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID - && contains(TOKEN_2022_FEE_ENTRIES, event.event_name.0.as_str()); + && contains(crate::MT_FEES_TOKEN_2022_FEE_ENTRIES, event.event_name.0.as_str()); } fn fee_domain(operation: &str) -> &'static str { @@ -195,7 +222,7 @@ pub fn materializer_fees_materialize_token_2022_state_snapshots( ), family: crate::MdMaterializedEventFamily::Fee, payload_json: serde_json::json!({ - "projectionVersion": 1, + "projectionVersion": crate::MT_FEES_PROJECTION_VERSION, "projectionSemantics": "authoritative_bounded_fee_state_snapshot", "domain": if confidential { "token_2022_confidential_transfer_fee_state" @@ -218,13 +245,27 @@ pub fn materializer_fees_materialize_token_2022_state_snapshots( "confidentialValuesDecrypted": false, "finalAccountStateCaptured": true, "provenance": { - "processorName": "materializer.fees", + "processorName": crate::MT_FEES_PROCESSOR_NAME, "processorVersion": env!("CARGO_PKG_VERSION"), "source": "bounded_token_2022_state_parser" } }), }); } + let status = if outputs.is_empty() { "ignored" } else { "inserted" }; + tracing::debug!( + target: crate::TRACING_TARGET_MATERIALIZER_FEES, + action = "materialize_token_2022_fee_state_snapshots", + component_name = crate::MT_FEES_COMPONENT_NAME, + processor_name = crate::MT_FEES_PROCESSOR_NAME, + processor_version = env!("CARGO_PKG_VERSION"), + account_key = %account_key, + slot, + state_kind, + output_count = outputs.len(), + status, + "materialize bounded Token-2022 fee state snapshots" + ); return std::result::Result::Ok(outputs); } @@ -273,7 +314,7 @@ mod tests { #[test] fn committed_token_2022_fee_instruction_creates_one_stable_output() { let observation = observation( - super::SPL_TOKEN_2022_SURFACE, + crate::MT_FEES_SPL_TOKEN_2022_SURFACE, kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID, "set_transfer_fee", false, @@ -290,7 +331,7 @@ mod tests { #[test] fn failed_token_2022_fee_instruction_is_refused() { let observation = observation( - super::SPL_TOKEN_2022_SURFACE, + crate::MT_FEES_SPL_TOKEN_2022_SURFACE, kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID, "harvest_withheld_tokens_to_mint", true, @@ -310,7 +351,7 @@ mod tests { false, ); let wrong_program = observation( - super::SPL_TOKEN_2022_SURFACE, + crate::MT_FEES_SPL_TOKEN_2022_SURFACE, kb_program_ids::SPL_TOKEN_PROGRAM_ID, "set_transfer_fee", false, @@ -334,7 +375,7 @@ mod tests { #[test] fn committed_confidential_fee_instruction_is_projected_without_decryption() { let observation = observation( - super::SPL_TOKEN_2022_SURFACE, + crate::MT_FEES_SPL_TOKEN_2022_SURFACE, kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID, "initialize_confidential_transfer_fee_config", false, diff --git a/kb-lib/src/materializer/governance.rs b/kb-lib/src/materializer/governance.rs index fc4efc0..6619f41 100644 --- a/kb-lib/src/materializer/governance.rs +++ b/kb-lib/src/materializer/governance.rs @@ -1,9 +1,17 @@ // file: kb-lib/src/materializer/governance.rs -// version: 3 +// version: 4 //! `governance` materializer family. -mod core; +mod constants; +mod materializer; /// Reserved governance materializer. -pub use self::core::MtGovernanceMaterializer; +pub use self::materializer::MtGovernanceMaterializer; + +/// Empty accepted-family boundary for the reserved component. +pub(crate) use self::constants::MT_GOVERNANCE_ACCEPTED_FAMILIES; +/// Stable runtime component name for the `governance` materializer. +pub(crate) use self::constants::MT_GOVERNANCE_COMPONENT_NAME; +/// Stable processor name for the `governance` materializer. +pub(crate) use self::constants::MT_GOVERNANCE_PROCESSOR_NAME; diff --git a/kb-lib/src/materializer/governance/constants.rs b/kb-lib/src/materializer/governance/constants.rs new file mode 100644 index 0000000..fcd74e4 --- /dev/null +++ b/kb-lib/src/materializer/governance/constants.rs @@ -0,0 +1,11 @@ +// file: kb-lib/src/materializer/governance/constants.rs +// version: 1 + +//! Local constants for the governance materializer. + +/// Stable runtime component name for the governance materializer. +pub(crate) const MT_GOVERNANCE_COMPONENT_NAME: &str = "kb-lib.materializer.governance"; +/// Stable processor name persisted by the replay ledger. +pub(crate) const MT_GOVERNANCE_PROCESSOR_NAME: &str = "materializer.governance"; +/// Empty family boundary retained while the materializer is reserved. +pub(crate) const MT_GOVERNANCE_ACCEPTED_FAMILIES: &[crate::MdEventFamily] = &[]; diff --git a/kb-lib/src/materializer/governance/core.rs b/kb-lib/src/materializer/governance/materializer.rs similarity index 72% rename from kb-lib/src/materializer/governance/core.rs rename to kb-lib/src/materializer/governance/materializer.rs index e7aa273..a17178e 100644 --- a/kb-lib/src/materializer/governance/core.rs +++ b/kb-lib/src/materializer/governance/materializer.rs @@ -1,5 +1,5 @@ -// file: kb-lib/src/materializer/governance/core.rs -// version: 4 +// file: kb-lib/src/materializer/governance/materializer.rs +// version: 5 //! Reserved governance materializer. @@ -9,7 +9,7 @@ pub struct MtGovernanceMaterializer; impl crate::MtMaterializer for crate::MtGovernanceMaterializer { fn materializer_name(&self) -> &'static str { - return "kb-lib.materializer.governance"; + return crate::MT_GOVERNANCE_COMPONENT_NAME; } fn materializer_version(&self) -> &'static str { @@ -22,27 +22,22 @@ impl crate::MtMaterializer for crate::MtGovernanceMaterializer { fn materialize_event( &self, - event: &crate::MdDecodedProtocolEvent, + _event: &crate::MdDecodedProtocolEvent, ) -> kb_core::Result> { - return std::result::Result::Ok(std::vec![crate::MdMaterializedEvent { - signature: event.signature.clone(), - slot: event.slot, - decoded_family: event.event_family, - materialized_family: crate::MdMaterializedEventFamily::Governance, - }]); + return std::result::Result::Ok(std::vec::Vec::new()); } } impl crate::MtApiEventMaterializer for crate::MtGovernanceMaterializer { fn identity(&self) -> crate::MtApiMaterializerIdentity { return crate::MtApiMaterializerIdentity { - name: "kb-lib.materializer.governance".to_string(), + name: crate::MT_GOVERNANCE_PROCESSOR_NAME.to_string(), version: env!("CARGO_PKG_VERSION").to_string(), }; } fn accepted_families(&self) -> &'static [crate::MdEventFamily] { - return &[]; + return crate::MT_GOVERNANCE_ACCEPTED_FAMILIES; } fn transaction_policy( diff --git a/kb-lib/src/materializer/lending.rs b/kb-lib/src/materializer/lending.rs index efd455f..506dd3f 100644 --- a/kb-lib/src/materializer/lending.rs +++ b/kb-lib/src/materializer/lending.rs @@ -1,9 +1,17 @@ // file: kb-lib/src/materializer/lending.rs -// version: 3 +// version: 4 //! `lending` materializer family. -mod core; +mod constants; +mod materializer; /// Reserved lending materializer. -pub use self::core::MtLendingMaterializer; +pub use self::materializer::MtLendingMaterializer; + +/// Empty accepted-family boundary for the reserved component. +pub(crate) use self::constants::MT_LENDING_ACCEPTED_FAMILIES; +/// Stable runtime component name for the `lending` materializer. +pub(crate) use self::constants::MT_LENDING_COMPONENT_NAME; +/// Stable processor name for the `lending` materializer. +pub(crate) use self::constants::MT_LENDING_PROCESSOR_NAME; diff --git a/kb-lib/src/materializer/lending/constants.rs b/kb-lib/src/materializer/lending/constants.rs new file mode 100644 index 0000000..03af06e --- /dev/null +++ b/kb-lib/src/materializer/lending/constants.rs @@ -0,0 +1,11 @@ +// file: kb-lib/src/materializer/lending/constants.rs +// version: 1 + +//! Local constants for the lending materializer. + +/// Stable runtime component name for the lending materializer. +pub(crate) const MT_LENDING_COMPONENT_NAME: &str = "kb-lib.materializer.lending"; +/// Stable processor name persisted by the replay ledger. +pub(crate) const MT_LENDING_PROCESSOR_NAME: &str = "materializer.lending"; +/// Empty family boundary retained while the materializer is reserved. +pub(crate) const MT_LENDING_ACCEPTED_FAMILIES: &[crate::MdEventFamily] = &[]; diff --git a/kb-lib/src/materializer/lending/core.rs b/kb-lib/src/materializer/lending/materializer.rs similarity index 72% rename from kb-lib/src/materializer/lending/core.rs rename to kb-lib/src/materializer/lending/materializer.rs index 511c2ad..1efe8d5 100644 --- a/kb-lib/src/materializer/lending/core.rs +++ b/kb-lib/src/materializer/lending/materializer.rs @@ -1,5 +1,5 @@ -// file: kb-lib/src/materializer/lending/core.rs -// version: 4 +// file: kb-lib/src/materializer/lending/materializer.rs +// version: 5 //! Reserved lending materializer. @@ -9,7 +9,7 @@ pub struct MtLendingMaterializer; impl crate::MtMaterializer for crate::MtLendingMaterializer { fn materializer_name(&self) -> &'static str { - return "kb-lib.materializer.lending"; + return crate::MT_LENDING_COMPONENT_NAME; } fn materializer_version(&self) -> &'static str { @@ -22,27 +22,22 @@ impl crate::MtMaterializer for crate::MtLendingMaterializer { fn materialize_event( &self, - event: &crate::MdDecodedProtocolEvent, + _event: &crate::MdDecodedProtocolEvent, ) -> kb_core::Result> { - return std::result::Result::Ok(std::vec![crate::MdMaterializedEvent { - signature: event.signature.clone(), - slot: event.slot, - decoded_family: event.event_family, - materialized_family: crate::MdMaterializedEventFamily::Lending, - }]); + return std::result::Result::Ok(std::vec::Vec::new()); } } impl crate::MtApiEventMaterializer for crate::MtLendingMaterializer { fn identity(&self) -> crate::MtApiMaterializerIdentity { return crate::MtApiMaterializerIdentity { - name: "kb-lib.materializer.lending".to_string(), + name: crate::MT_LENDING_PROCESSOR_NAME.to_string(), version: env!("CARGO_PKG_VERSION").to_string(), }; } fn accepted_families(&self) -> &'static [crate::MdEventFamily] { - return &[]; + return crate::MT_LENDING_ACCEPTED_FAMILIES; } fn transaction_policy( diff --git a/kb-lib/src/materializer/lifecycle.rs b/kb-lib/src/materializer/lifecycle.rs index cb18e30..d0b9655 100644 --- a/kb-lib/src/materializer/lifecycle.rs +++ b/kb-lib/src/materializer/lifecycle.rs @@ -1,13 +1,59 @@ // file: kb-lib/src/materializer/lifecycle.rs -// version: 4 +// version: 5 //! `lifecycle` materializer family. mod constants; -mod core; +mod materializer; /// Stable native lifecycle materializer. -pub use self::core::MtLifecycleMaterializer; +pub use self::materializer::MtLifecycleMaterializer; -/// Canonical tracing target for the native lifecycle materializer. +/// Crate-visible `MT_LIFECYCLE_ACCEPTED_FAMILIES` component constant. +pub(crate) use self::constants::MT_LIFECYCLE_ACCEPTED_FAMILIES; +/// Crate-visible `MT_LIFECYCLE_ADDRESS_LOOKUP_TABLE_ENTRIES` component constant. +pub(crate) use self::constants::MT_LIFECYCLE_ADDRESS_LOOKUP_TABLE_ENTRIES; +/// Crate-visible `MT_LIFECYCLE_ADDRESS_LOOKUP_TABLE_SURFACE` component constant. +pub(crate) use self::constants::MT_LIFECYCLE_ADDRESS_LOOKUP_TABLE_SURFACE; +/// Crate-visible `MT_LIFECYCLE_BPF_LOADER_DEPRECATED_SURFACE` component constant. +pub(crate) use self::constants::MT_LIFECYCLE_BPF_LOADER_DEPRECATED_SURFACE; +/// Crate-visible `MT_LIFECYCLE_BPF_LOADER_SURFACE` component constant. +pub(crate) use self::constants::MT_LIFECYCLE_BPF_LOADER_SURFACE; +/// Crate-visible `MT_LIFECYCLE_BPF_LOADER_UPGRADEABLE_SURFACE` component constant. +pub(crate) use self::constants::MT_LIFECYCLE_BPF_LOADER_UPGRADEABLE_SURFACE; +/// Stable runtime component name for the `lifecycle` materializer. +pub(crate) use self::constants::MT_LIFECYCLE_COMPONENT_NAME; +/// Crate-visible `MT_LIFECYCLE_FEATURE_ENTRIES` component constant. +pub(crate) use self::constants::MT_LIFECYCLE_FEATURE_ENTRIES; +/// Crate-visible `MT_LIFECYCLE_FEATURE_SURFACE` component constant. +pub(crate) use self::constants::MT_LIFECYCLE_FEATURE_SURFACE; +/// Crate-visible `MT_LIFECYCLE_IMMUTABLE_LOADER_ENTRIES` component constant. +pub(crate) use self::constants::MT_LIFECYCLE_IMMUTABLE_LOADER_ENTRIES; +/// Crate-visible `MT_LIFECYCLE_LOADER_V4_ENTRIES` component constant. +pub(crate) use self::constants::MT_LIFECYCLE_LOADER_V4_ENTRIES; +/// Crate-visible `MT_LIFECYCLE_LOADER_V4_SURFACE` component constant. +pub(crate) use self::constants::MT_LIFECYCLE_LOADER_V4_SURFACE; +/// Stable processor name for the `lifecycle` materializer. +pub(crate) use self::constants::MT_LIFECYCLE_PROCESSOR_NAME; +/// Stable projection version for the `lifecycle` materializer. +pub(crate) use self::constants::MT_LIFECYCLE_PROJECTION_VERSION; +/// Crate-visible `MT_LIFECYCLE_SLASHING_ENTRIES` component constant. +pub(crate) use self::constants::MT_LIFECYCLE_SLASHING_ENTRIES; +/// Crate-visible `MT_LIFECYCLE_SLASHING_SURFACE` component constant. +pub(crate) use self::constants::MT_LIFECYCLE_SLASHING_SURFACE; +/// Crate-visible `MT_LIFECYCLE_SYSTEM_ACCOUNT_ENTRIES` component constant. +pub(crate) use self::constants::MT_LIFECYCLE_SYSTEM_ACCOUNT_ENTRIES; +/// Crate-visible `MT_LIFECYCLE_SYSTEM_NONCE_ENTRIES` component constant. +pub(crate) use self::constants::MT_LIFECYCLE_SYSTEM_NONCE_ENTRIES; +/// Crate-visible `MT_LIFECYCLE_SYSTEM_SURFACE` component constant. +pub(crate) use self::constants::MT_LIFECYCLE_SYSTEM_SURFACE; +/// Crate-visible `MT_LIFECYCLE_UPGRADEABLE_LOADER_ENTRIES` component constant. +pub(crate) use self::constants::MT_LIFECYCLE_UPGRADEABLE_LOADER_ENTRIES; +/// Crate-visible `MT_LIFECYCLE_ZK_ELGAMAL_CLOSE_ENTRIES` component constant. +pub(crate) use self::constants::MT_LIFECYCLE_ZK_ELGAMAL_CLOSE_ENTRIES; +/// Crate-visible `MT_LIFECYCLE_ZK_ELGAMAL_PROOF_SURFACE` component constant. +pub(crate) use self::constants::MT_LIFECYCLE_ZK_ELGAMAL_PROOF_SURFACE; +/// Crate-visible `MT_LIFECYCLE_ZK_ELGAMAL_VERIFY_ENTRIES` component constant. +pub(crate) use self::constants::MT_LIFECYCLE_ZK_ELGAMAL_VERIFY_ENTRIES; +/// Canonical tracing target for the `lifecycle` materializer. pub(crate) use self::constants::TRACING_TARGET_MATERIALIZER_LIFECYCLE; diff --git a/kb-lib/src/materializer/lifecycle/constants.rs b/kb-lib/src/materializer/lifecycle/constants.rs index 36539a3..1ee82cb 100644 --- a/kb-lib/src/materializer/lifecycle/constants.rs +++ b/kb-lib/src/materializer/lifecycle/constants.rs @@ -1,7 +1,100 @@ // file: kb-lib/src/materializer/lifecycle/constants.rs -// version: 2 +// version: 4 -//! Local constants for the native lifecycle materializer. +//! Local constants for the lifecycle materializer. -/// Canonical tracing target for this component. +/// Stable runtime component name for the lifecycle materializer. +pub(crate) const MT_LIFECYCLE_COMPONENT_NAME: &str = "kb-lib.materializer.lifecycle"; +/// Stable processor name persisted by the replay ledger. +pub(crate) const MT_LIFECYCLE_PROCESSOR_NAME: &str = "materializer.lifecycle"; +/// Stable projection contract version for the lifecycle materializer. +pub(crate) const MT_LIFECYCLE_PROJECTION_VERSION: u32 = 2; +/// Canonical tracing target for the lifecycle materializer. pub(crate) const TRACING_TARGET_MATERIALIZER_LIFECYCLE: &str = "kb-lib.materializer.lifecycle"; + +/// Component contract for `accepted_families`. +pub(crate) const MT_LIFECYCLE_ACCEPTED_FAMILIES: &[crate::MdEventFamily] = &[ + crate::MdEventFamily::Lifecycle, + crate::MdEventFamily::Admin, + crate::MdEventFamily::Audit, +]; +/// Component contract for `address_lookup_table_surface`. +pub(crate) const MT_LIFECYCLE_ADDRESS_LOOKUP_TABLE_SURFACE: &str = + "solana.core.address_lookup_table"; +/// Component contract for `address_lookup_table_entries`. +pub(crate) const MT_LIFECYCLE_ADDRESS_LOOKUP_TABLE_ENTRIES: &[&str] = &[ + "create_lookup_table", + "freeze_lookup_table", + "extend_lookup_table", + "deactivate_lookup_table", + "close_lookup_table", +]; +/// Component contract for `bpf_loader_deprecated_surface`. +pub(crate) const MT_LIFECYCLE_BPF_LOADER_DEPRECATED_SURFACE: &str = + "solana.core.bpf_loader_deprecated"; +/// Component contract for `bpf_loader_surface`. +pub(crate) const MT_LIFECYCLE_BPF_LOADER_SURFACE: &str = "solana.core.bpf_loader"; +/// Component contract for `bpf_loader_upgradeable_surface`. +pub(crate) const MT_LIFECYCLE_BPF_LOADER_UPGRADEABLE_SURFACE: &str = + "solana.core.bpf_loader_upgradeable"; +/// Component contract for `loader_v4_surface`. +pub(crate) const MT_LIFECYCLE_LOADER_V4_SURFACE: &str = "solana.core.loader_v4"; +/// Component contract for `feature_surface`. +pub(crate) const MT_LIFECYCLE_FEATURE_SURFACE: &str = "solana.core.feature"; +/// Component contract for `feature_entries`. +pub(crate) const MT_LIFECYCLE_FEATURE_ENTRIES: &[&str] = &["revoke_pending_activation"]; +/// Component contract for `immutable_loader_entries`. +pub(crate) const MT_LIFECYCLE_IMMUTABLE_LOADER_ENTRIES: &[&str] = &["finalize"]; +/// Component contract for `upgradeable_loader_entries`. +pub(crate) const MT_LIFECYCLE_UPGRADEABLE_LOADER_ENTRIES: &[&str] = &[ + "initialize_buffer", + "deploy_with_max_data_len", + "upgrade", + "close", + "extend_program", +]; +/// Component contract for `loader_v4_entries`. +pub(crate) const MT_LIFECYCLE_LOADER_V4_ENTRIES: &[&str] = + &["set_program_length", "deploy", "retract", "finalize"]; +/// Component contract for `slashing_surface`. +pub(crate) const MT_LIFECYCLE_SLASHING_SURFACE: &str = "solana.core.slashing"; +/// Component contract for `slashing_entries`. +pub(crate) const MT_LIFECYCLE_SLASHING_ENTRIES: &[&str] = + &["close_violation_report", "duplicate_block_proof"]; +/// Component contract for `system_surface`. +pub(crate) const MT_LIFECYCLE_SYSTEM_SURFACE: &str = "solana.core.system"; +/// Component contract for `system_account_entries`. +pub(crate) const MT_LIFECYCLE_SYSTEM_ACCOUNT_ENTRIES: &[&str] = &[ + "create_account", + "create_account_with_seed", + "allocate", + "allocate_with_seed", + "create_account_allow_prefund", +]; +/// Component contract for `system_nonce_entries`. +pub(crate) const MT_LIFECYCLE_SYSTEM_NONCE_ENTRIES: &[&str] = &[ + "advance_nonce_account", + "withdraw_nonce_account", + "initialize_nonce_account", + "authorize_nonce_account", + "upgrade_nonce_account", +]; +/// Component contract for `zk_elgamal_proof_surface`. +pub(crate) const MT_LIFECYCLE_ZK_ELGAMAL_PROOF_SURFACE: &str = "solana.core.zk_elgamal_proof"; +/// Component contract for `zk_elgamal_close_entries`. +pub(crate) const MT_LIFECYCLE_ZK_ELGAMAL_CLOSE_ENTRIES: &[&str] = &["close_context_state"]; +/// Component contract for `zk_elgamal_verify_entries`. +pub(crate) const MT_LIFECYCLE_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", +]; diff --git a/kb-lib/src/materializer/lifecycle/core.rs b/kb-lib/src/materializer/lifecycle/materializer.rs similarity index 88% rename from kb-lib/src/materializer/lifecycle/core.rs rename to kb-lib/src/materializer/lifecycle/materializer.rs index 1017c0a..20e217b 100644 --- a/kb-lib/src/materializer/lifecycle/core.rs +++ b/kb-lib/src/materializer/lifecycle/materializer.rs @@ -1,70 +1,8 @@ -// file: kb-lib/src/materializer/lifecycle/core.rs -// version: 16 +// file: kb-lib/src/materializer/lifecycle/materializer.rs +// version: 18 //! Stable lifecycle projections derived from exact decoded observations. -const ACCEPTED_FAMILIES: &[crate::MdEventFamily] = &[ - crate::MdEventFamily::Lifecycle, - crate::MdEventFamily::Admin, - crate::MdEventFamily::Audit, -]; -const ADDRESS_LOOKUP_TABLE_SURFACE: &str = "solana.core.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.core.bpf_loader_deprecated"; -const BPF_LOADER_SURFACE: &str = "solana.core.bpf_loader"; -const BPF_LOADER_UPGRADEABLE_SURFACE: &str = "solana.core.bpf_loader_upgradeable"; -const LOADER_V4_SURFACE: &str = "solana.core.loader_v4"; -const FEATURE_SURFACE: &str = "solana.core.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.core.slashing"; -const SLASHING_ENTRIES: &[&str] = &["close_violation_report", "duplicate_block_proof"]; -const SYSTEM_SURFACE: &str = "solana.core.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.core.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", -]; - #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum ProjectionKind { AddressLookupTable, @@ -125,7 +63,7 @@ pub struct MtLifecycleMaterializer; impl crate::MtMaterializer for crate::MtLifecycleMaterializer { fn materializer_name(&self) -> &'static str { - return "kb-lib.materializer.lifecycle"; + return crate::MT_LIFECYCLE_COMPONENT_NAME; } fn materializer_version(&self) -> &'static str { @@ -152,13 +90,13 @@ impl crate::MtMaterializer for crate::MtLifecycleMaterializer { impl crate::MtApiEventMaterializer for crate::MtLifecycleMaterializer { fn identity(&self) -> crate::MtApiMaterializerIdentity { return crate::MtApiMaterializerIdentity { - name: "materializer.lifecycle".to_string(), + name: crate::MT_LIFECYCLE_PROCESSOR_NAME.to_string(), version: env!("CARGO_PKG_VERSION").to_string(), }; } fn accepted_families(&self) -> &'static [crate::MdEventFamily] { - return ACCEPTED_FAMILIES; + return crate::MT_LIFECYCLE_ACCEPTED_FAMILIES; } fn accepts_observation(&self, observation: &crate::DcApiDecodedObservation) -> bool { @@ -239,7 +177,7 @@ impl crate::MtApiEventMaterializer for crate::MtLifecycleMaterializer { output_key, family: crate::MdMaterializedEventFamily::Lifecycle, payload_json: serde_json::json!({ - "projectionVersion": 1, + "projectionVersion": crate::MT_LIFECYCLE_PROJECTION_VERSION, "projectionSemantics": "committed_instruction_lifecycle_event", "domain": domain, "loaderSurface": loader_surface, @@ -249,6 +187,10 @@ impl crate::MtApiEventMaterializer for crate::MtLifecycleMaterializer { "slot": observation.event.slot.0, "instructionPath": observation.event.instruction_path.0.clone(), "transactionSucceeded": true, + "provenance": { + "processorName": crate::MT_LIFECYCLE_PROCESSOR_NAME, + "processorVersion": env!("CARGO_PKG_VERSION"), + }, "accounts": accounts, "parameters": parameters, "projection": projection_details, @@ -263,7 +205,9 @@ impl crate::MtApiEventMaterializer for crate::MtLifecycleMaterializer { } fn family_is_accepted(family: crate::MdEventFamily) -> bool { - return ACCEPTED_FAMILIES.iter().any(|accepted| return *accepted == family); + return crate::MT_LIFECYCLE_ACCEPTED_FAMILIES + .iter() + .any(|accepted| return *accepted == family); } fn observation_projection( @@ -275,8 +219,8 @@ fn observation_projection( if static_projection.is_some() { return static_projection; } - if surface_code == ZK_ELGAMAL_PROOF_SURFACE - && contains(ZK_ELGAMAL_VERIFY_ENTRIES, entry_code) + if surface_code == crate::MT_LIFECYCLE_ZK_ELGAMAL_PROOF_SURFACE + && contains(crate::MT_LIFECYCLE_ZK_ELGAMAL_VERIFY_ENTRIES, entry_code) && observation .payload_json .get("parameters") @@ -290,48 +234,63 @@ fn observation_projection( } fn static_projection(surface_code: &str, entry_code: &str) -> std::option::Option { - if surface_code == FEATURE_SURFACE && contains(FEATURE_ENTRIES, entry_code) { + if surface_code == crate::MT_LIFECYCLE_FEATURE_SURFACE + && contains(crate::MT_LIFECYCLE_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) + if surface_code == crate::MT_LIFECYCLE_ADDRESS_LOOKUP_TABLE_SURFACE + && contains(crate::MT_LIFECYCLE_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) + if (surface_code == crate::MT_LIFECYCLE_BPF_LOADER_DEPRECATED_SURFACE + || surface_code == crate::MT_LIFECYCLE_BPF_LOADER_SURFACE) + && contains(crate::MT_LIFECYCLE_IMMUTABLE_LOADER_ENTRIES, entry_code) { - let loader_surface = if surface_code == BPF_LOADER_DEPRECATED_SURFACE { - BPF_LOADER_DEPRECATED_SURFACE + let loader_surface = if surface_code == crate::MT_LIFECYCLE_BPF_LOADER_DEPRECATED_SURFACE { + crate::MT_LIFECYCLE_BPF_LOADER_DEPRECATED_SURFACE } else { - BPF_LOADER_SURFACE + crate::MT_LIFECYCLE_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) + if surface_code == crate::MT_LIFECYCLE_BPF_LOADER_UPGRADEABLE_SURFACE + && contains(crate::MT_LIFECYCLE_UPGRADEABLE_LOADER_ENTRIES, entry_code) { return std::option::Option::Some(ProjectionKind::ProgramLoader( - BPF_LOADER_UPGRADEABLE_SURFACE, + crate::MT_LIFECYCLE_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 == crate::MT_LIFECYCLE_LOADER_V4_SURFACE + && contains(crate::MT_LIFECYCLE_LOADER_V4_ENTRIES, entry_code) + { + return std::option::Option::Some(ProjectionKind::ProgramLoader( + crate::MT_LIFECYCLE_LOADER_V4_SURFACE, + )); } - if surface_code == SLASHING_SURFACE && contains(SLASHING_ENTRIES, entry_code) { + if surface_code == crate::MT_LIFECYCLE_SLASHING_SURFACE + && contains(crate::MT_LIFECYCLE_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) { + if surface_code == crate::MT_LIFECYCLE_SYSTEM_SURFACE + && contains(crate::MT_LIFECYCLE_SYSTEM_ACCOUNT_ENTRIES, entry_code) + { return std::option::Option::Some(ProjectionKind::SystemAccountLifecycle); } - if surface_code == SYSTEM_SURFACE && contains(SYSTEM_NONCE_ENTRIES, entry_code) { + if surface_code == crate::MT_LIFECYCLE_SYSTEM_SURFACE + && contains(crate::MT_LIFECYCLE_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) { + if surface_code == crate::MT_LIFECYCLE_ZK_ELGAMAL_PROOF_SURFACE + && contains(crate::MT_LIFECYCLE_ZK_ELGAMAL_CLOSE_ENTRIES, entry_code) + { return std::option::Option::Some(ProjectionKind::ZkProofContextClosure); } return std::option::Option::None; @@ -539,7 +498,7 @@ mod tests { transaction_failed: bool, ) -> crate::DcApiDecodedObservation { return observation_with_payload( - super::SYSTEM_SURFACE, + crate::MT_LIFECYCLE_SYSTEM_SURFACE, entry_code, family, transaction_failed, @@ -579,7 +538,7 @@ mod tests { ) }; return observation_with_payload( - super::SLASHING_SURFACE, + crate::MT_LIFECYCLE_SLASHING_SURFACE, entry_code, family, transaction_failed, @@ -620,7 +579,7 @@ mod tests { crate::MdEventFamily::Audit }; return observation_with_payload( - super::ZK_ELGAMAL_PROOF_SURFACE, + crate::MT_LIFECYCLE_ZK_ELGAMAL_PROOF_SURFACE, entry_code, family, transaction_failed, @@ -637,7 +596,11 @@ mod tests { let materializer = crate::MtLifecycleMaterializer; let result = crate::MtApiEventMaterializer::materialize( &materializer, - &observation(super::ADDRESS_LOOKUP_TABLE_SURFACE, "create_lookup_table", false), + &observation( + crate::MT_LIFECYCLE_ADDRESS_LOOKUP_TABLE_SURFACE, + "create_lookup_table", + false, + ), ); assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted); assert_eq!(result.outputs.len(), 1); @@ -651,7 +614,11 @@ mod tests { let materializer = crate::MtLifecycleMaterializer; let result = crate::MtApiEventMaterializer::materialize( &materializer, - &observation(super::BPF_LOADER_UPGRADEABLE_SURFACE, "deploy_with_max_data_len", false), + &observation( + crate::MT_LIFECYCLE_BPF_LOADER_UPGRADEABLE_SURFACE, + "deploy_with_max_data_len", + false, + ), ); assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted); assert_eq!( @@ -661,7 +628,7 @@ mod tests { assert_eq!(result.outputs[0].payload_json["domain"], "program_loader"); assert_eq!( result.outputs[0].payload_json["loaderSurface"], - super::BPF_LOADER_UPGRADEABLE_SURFACE + crate::MT_LIFECYCLE_BPF_LOADER_UPGRADEABLE_SURFACE ); } @@ -670,7 +637,7 @@ mod tests { let materializer = crate::MtLifecycleMaterializer; let result = crate::MtApiEventMaterializer::materialize( &materializer, - &observation(super::FEATURE_SURFACE, "revoke_pending_activation", false), + &observation(crate::MT_LIFECYCLE_FEATURE_SURFACE, "revoke_pending_activation", false), ); assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted); assert_eq!(result.outputs[0].output_key, "feature_gate:revoke_pending_activation:0"); @@ -696,7 +663,7 @@ mod tests { ), ] { let observation = observation_with_payload( - super::SYSTEM_SURFACE, + crate::MT_LIFECYCLE_SYSTEM_SURFACE, operation, crate::MdEventFamily::Lifecycle, false, @@ -724,7 +691,7 @@ mod tests { fn system_account_creation_does_not_duplicate_balance_changes() { let materializer = crate::MtLifecycleMaterializer; let observation = observation_with_payload( - super::SYSTEM_SURFACE, + crate::MT_LIFECYCLE_SYSTEM_SURFACE, "create_account", crate::MdEventFamily::Lifecycle, false, @@ -895,7 +862,8 @@ mod tests { fn loader_write_and_admin_events_are_not_lifecycle_projections() { let materializer = crate::MtLifecycleMaterializer; for entry_code in ["write", "set_authority", "transfer_authority"] { - let observation = observation(super::BPF_LOADER_UPGRADEABLE_SURFACE, entry_code, false); + let observation = + observation(crate::MT_LIFECYCLE_BPF_LOADER_UPGRADEABLE_SURFACE, entry_code, false); assert!(!crate::MtApiEventMaterializer::accepts_observation( &materializer, &observation, @@ -908,7 +876,7 @@ mod tests { let materializer = crate::MtLifecycleMaterializer; let result = crate::MtApiEventMaterializer::materialize( &materializer, - &observation(super::SYSTEM_SURFACE, "transfer", false), + &observation(crate::MT_LIFECYCLE_SYSTEM_SURFACE, "transfer", false), ); assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Ignored); } @@ -934,8 +902,12 @@ mod tests { fn failed_native_lifecycle_is_refused() { let materializer = crate::MtLifecycleMaterializer; let observations = [ - observation(super::ADDRESS_LOOKUP_TABLE_SURFACE, "close_lookup_table", true), - observation(super::LOADER_V4_SURFACE, "deploy", true), + observation( + crate::MT_LIFECYCLE_ADDRESS_LOOKUP_TABLE_SURFACE, + "close_lookup_table", + true, + ), + observation(crate::MT_LIFECYCLE_LOADER_V4_SURFACE, "deploy", true), nonce_observation("authorize_nonce_account", crate::MdEventFamily::Admin, true), zk_observation("verify_zero_ciphertext", true, true), slashing_observation("duplicate_block_proof", true), diff --git a/kb-lib/src/materializer/liquidity.rs b/kb-lib/src/materializer/liquidity.rs index 7cb09e2..fd972a1 100644 --- a/kb-lib/src/materializer/liquidity.rs +++ b/kb-lib/src/materializer/liquidity.rs @@ -1,9 +1,17 @@ // file: kb-lib/src/materializer/liquidity.rs -// version: 3 +// version: 4 //! `liquidity` materializer family. -mod core; +mod constants; +mod materializer; /// Reserved liquidity materializer. -pub use self::core::MtLiquidityMaterializer; +pub use self::materializer::MtLiquidityMaterializer; + +/// Empty accepted-family boundary for the reserved component. +pub(crate) use self::constants::MT_LIQUIDITY_ACCEPTED_FAMILIES; +/// Stable runtime component name for the `liquidity` materializer. +pub(crate) use self::constants::MT_LIQUIDITY_COMPONENT_NAME; +/// Stable processor name for the `liquidity` materializer. +pub(crate) use self::constants::MT_LIQUIDITY_PROCESSOR_NAME; diff --git a/kb-lib/src/materializer/liquidity/constants.rs b/kb-lib/src/materializer/liquidity/constants.rs new file mode 100644 index 0000000..9060e8a --- /dev/null +++ b/kb-lib/src/materializer/liquidity/constants.rs @@ -0,0 +1,11 @@ +// file: kb-lib/src/materializer/liquidity/constants.rs +// version: 1 + +//! Local constants for the liquidity materializer. + +/// Stable runtime component name for the liquidity materializer. +pub(crate) const MT_LIQUIDITY_COMPONENT_NAME: &str = "kb-lib.materializer.liquidity"; +/// Stable processor name persisted by the replay ledger. +pub(crate) const MT_LIQUIDITY_PROCESSOR_NAME: &str = "materializer.liquidity"; +/// Empty family boundary retained while the materializer is reserved. +pub(crate) const MT_LIQUIDITY_ACCEPTED_FAMILIES: &[crate::MdEventFamily] = &[]; diff --git a/kb-lib/src/materializer/liquidity/core.rs b/kb-lib/src/materializer/liquidity/materializer.rs similarity index 72% rename from kb-lib/src/materializer/liquidity/core.rs rename to kb-lib/src/materializer/liquidity/materializer.rs index 7bd7a8e..a739baf 100644 --- a/kb-lib/src/materializer/liquidity/core.rs +++ b/kb-lib/src/materializer/liquidity/materializer.rs @@ -1,5 +1,5 @@ -// file: kb-lib/src/materializer/liquidity/core.rs -// version: 4 +// file: kb-lib/src/materializer/liquidity/materializer.rs +// version: 5 //! Reserved liquidity materializer. @@ -9,7 +9,7 @@ pub struct MtLiquidityMaterializer; impl crate::MtMaterializer for crate::MtLiquidityMaterializer { fn materializer_name(&self) -> &'static str { - return "kb-lib.materializer.liquidity"; + return crate::MT_LIQUIDITY_COMPONENT_NAME; } fn materializer_version(&self) -> &'static str { @@ -22,27 +22,22 @@ impl crate::MtMaterializer for crate::MtLiquidityMaterializer { fn materialize_event( &self, - event: &crate::MdDecodedProtocolEvent, + _event: &crate::MdDecodedProtocolEvent, ) -> kb_core::Result> { - return std::result::Result::Ok(std::vec![crate::MdMaterializedEvent { - signature: event.signature.clone(), - slot: event.slot, - decoded_family: event.event_family, - materialized_family: crate::MdMaterializedEventFamily::Liquidity, - }]); + return std::result::Result::Ok(std::vec::Vec::new()); } } impl crate::MtApiEventMaterializer for crate::MtLiquidityMaterializer { fn identity(&self) -> crate::MtApiMaterializerIdentity { return crate::MtApiMaterializerIdentity { - name: "kb-lib.materializer.liquidity".to_string(), + name: crate::MT_LIQUIDITY_PROCESSOR_NAME.to_string(), version: env!("CARGO_PKG_VERSION").to_string(), }; } fn accepted_families(&self) -> &'static [crate::MdEventFamily] { - return &[]; + return crate::MT_LIQUIDITY_ACCEPTED_FAMILIES; } fn transaction_policy( diff --git a/kb-lib/src/materializer/metadata.rs b/kb-lib/src/materializer/metadata.rs index 9e9acba..40d6282 100644 --- a/kb-lib/src/materializer/metadata.rs +++ b/kb-lib/src/materializer/metadata.rs @@ -1,31 +1,52 @@ // file: kb-lib/src/materializer/metadata.rs -// version: 5 +// version: 8 //! `metadata` materializer family. -mod core; +mod metaplex_token_metadata; mod solana_program_metadata; +mod token_2022; -/// Token-2022, Metaplex and Solana Program Metadata materializer. -pub use self::core::MtMetadataMaterializer; /// Materializes one canonical non-Metadata Metaplex account snapshot. -pub use self::core::materializer_metadata_materialize_metaplex_account_snapshot; +pub use self::metaplex_token_metadata::materializer_metadata_materialize_metaplex_account_snapshot; /// Materializes one canonical Metaplex Metadata account snapshot. -pub use self::core::materializer_metadata_materialize_metaplex_metadata_snapshot; -/// Materializes Token-2022 embedded metadata and group snapshots. -pub use self::core::materializer_metadata_materialize_token_2022_snapshot; +pub use self::metaplex_token_metadata::materializer_metadata_materialize_metaplex_metadata_snapshot; +/// Compatibility materializer retaining the historical generic metadata type name. +pub use self::solana_program_metadata::MtMetadataMaterializer; +/// Stable Solana Program Metadata instruction materializer. +pub use self::solana_program_metadata::MtSolanaProgramMetadataMaterializer; /// Materializes one decoded Solana Program Metadata account snapshot. pub use self::solana_program_metadata::materializer_metadata_materialize_solana_program_metadata_account_snapshot; /// Materializes one bounded Solana Program Metadata Buffer account snapshot. pub use self::solana_program_metadata::materializer_metadata_materialize_solana_program_metadata_buffer_snapshot; /// Materializes one bounded Solana Program Metadata Metadata account snapshot. pub use self::solana_program_metadata::materializer_metadata_materialize_solana_program_metadata_metadata_snapshot; +/// Materializes Token-2022 embedded metadata and group snapshots. +pub use self::token_2022::materializer_metadata_materialize_token_2022_snapshot; +/// Stable runtime component name for Metaplex Token Metadata projections. +pub(crate) use self::metaplex_token_metadata::MT_METADATA_METAPLEX_TOKEN_METADATA_COMPONENT_NAME; +/// Stable processor name for Metaplex Token Metadata projections. +pub(crate) use self::metaplex_token_metadata::MT_METADATA_METAPLEX_TOKEN_METADATA_PROCESSOR_NAME; +/// Stable Metaplex Token Metadata projection version. +pub(crate) use self::metaplex_token_metadata::MT_METADATA_METAPLEX_TOKEN_METADATA_PROJECTION_VERSION; +/// Canonical tracing target for Metaplex Token Metadata materialization. +pub(crate) use self::metaplex_token_metadata::TRACING_TARGET_MATERIALIZER_METADATA_METAPLEX_TOKEN_METADATA; /// Event families accepted by the Solana Program Metadata instruction materializer. -pub(crate) use self::solana_program_metadata::MT_METADATA_SPM_ACCEPTED_FAMILIES; +pub(crate) use self::solana_program_metadata::MT_METADATA_SOLANA_PROGRAM_METADATA_ACCEPTED_FAMILIES; +/// Stable runtime component name for Solana Program Metadata projections. +pub(crate) use self::solana_program_metadata::MT_METADATA_SOLANA_PROGRAM_METADATA_COMPONENT_NAME; /// Stable processor name for Solana Program Metadata projections. -pub(crate) use self::solana_program_metadata::MT_METADATA_SPM_PROCESSOR_NAME; +pub(crate) use self::solana_program_metadata::MT_METADATA_SOLANA_PROGRAM_METADATA_PROCESSOR_NAME; /// Stable projection schema version for Solana Program Metadata outputs. -pub(crate) use self::solana_program_metadata::MT_METADATA_SPM_PROJECTION_VERSION; +pub(crate) use self::solana_program_metadata::MT_METADATA_SOLANA_PROGRAM_METADATA_PROJECTION_VERSION; /// Canonical tracing target for Solana Program Metadata materialization. pub(crate) use self::solana_program_metadata::TRACING_TARGET_MATERIALIZER_METADATA_SOLANA_PROGRAM_METADATA; +/// Stable runtime component name for Token-2022 embedded metadata projections. +pub(crate) use self::token_2022::MT_METADATA_TOKEN_2022_COMPONENT_NAME; +/// Stable processor name for Token-2022 embedded metadata projections. +pub(crate) use self::token_2022::MT_METADATA_TOKEN_2022_PROCESSOR_NAME; +/// Stable Token-2022 embedded metadata projection version. +pub(crate) use self::token_2022::MT_METADATA_TOKEN_2022_PROJECTION_VERSION; +/// Canonical tracing target for Token-2022 embedded metadata materialization. +pub(crate) use self::token_2022::TRACING_TARGET_MATERIALIZER_METADATA_TOKEN_2022; diff --git a/kb-lib/src/materializer/metadata/core.rs b/kb-lib/src/materializer/metadata/core.rs deleted file mode 100644 index 5591bb7..0000000 --- a/kb-lib/src/materializer/metadata/core.rs +++ /dev/null @@ -1,608 +0,0 @@ -// file: kb-lib/src/materializer/metadata/core.rs -// version: 7 - -//! Stable Token-2022, Metaplex and Solana Program Metadata materializer. - -/// Initial materializer implementation placeholder. -#[derive(Clone, Debug, Default)] -pub struct MtMetadataMaterializer; - -impl crate::MtMaterializer for crate::MtMetadataMaterializer { - fn materializer_name(&self) -> &'static str { - return "kb-lib.materializer.metadata"; - } - - fn materializer_version(&self) -> &'static str { - return env!("CARGO_PKG_VERSION"); - } - - fn accepts_event(&self, event: &crate::MdDecodedProtocolEvent) -> bool { - return event.event_family == crate::MdEventFamily::Metadata - && event.program_id.0 == kb_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID - && event.surface_code.0 == crate::DC_METADATA_SPM_SURFACE_CODE - && crate::DC_METADATA_SPM_INSTRUCTION_ENTRIES - .iter() - .any(|(_, code)| return *code == event.event_name.0.as_str()); - } - - fn materialize_event( - &self, - event: &crate::MdDecodedProtocolEvent, - ) -> kb_core::Result> { - let materialized = crate::MdMaterializedEvent { - signature: event.signature.clone(), - slot: event.slot, - decoded_family: event.event_family, - materialized_family: crate::MdMaterializedEventFamily::Metadata, - }; - return std::result::Result::Ok(std::vec![materialized]); - } -} - -/// Materialize embedded Token-2022 metadata, group, and group-member extensions. -pub fn materializer_metadata_materialize_token_2022_snapshot( - account_key: &str, - slot: u64, - state: &crate::DcToken2022State, -) -> std::result::Result, String> { - if account_key.is_empty() { - return std::result::Result::Err( - "Token-2022 metadata snapshot requires a non-empty account key".to_string(), - ); - } - if state.kind != crate::DcToken2022StateKind::Mint { - return std::result::Result::Err( - "Token-2022 metadata snapshot requires a parsed mint state".to_string(), - ); - } - let mut outputs = std::vec::Vec::new(); - for entry in &state.extensions { - let projection_kind = match entry.extension_name { - "token_metadata" => std::option::Option::Some("token_metadata"), - "token_group" => std::option::Option::Some("token_group"), - "token_group_member" => std::option::Option::Some("token_group_member"), - _ => std::option::Option::None, - }; - let kind = match projection_kind { - std::option::Option::Some(value) => value, - std::option::Option::None => continue, - }; - outputs.push(crate::MtApiMaterializedOutput { - output_key: format!("token_2022_metadata_snapshot:{account_key}:{slot}:{kind}"), - family: crate::MdMaterializedEventFamily::Metadata, - payload_json: serde_json::json!({ - "projectionVersion": 1, - "domain": "spl_token_2022_embedded_metadata", - "projectionSemantics": "authoritative_bounded_mint_extension_snapshot", - "idempotenceKey": format!("token_2022_metadata:{account_key}:{slot}:{kind}"), - "accountKey": account_key, - "slot": slot, - "programId": kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID, - "metadataSource": "token_2022_embedded_tlv", - "projectionKind": kind, - "extensionType": entry.extension_type, - "extensionName": entry.extension_name, - "valueHex": entry.value_hex, - "valueFields": entry.value_fields, - "externalUriFetched": false, - "identityAuthority": "mint_address", - "finalAccountStateCaptured": true, - "provenance": { - "processorName": "kb-lib.materializer.metadata", - "processorVersion": env!("CARGO_PKG_VERSION"), - "source": "bounded_token_2022_state_parser" - } - }), - }); - } - return std::result::Result::Ok(outputs); -} - -/// Materialize one canonical Metaplex `MetadataV1` account snapshot. -/// -/// Account snapshots are authoritative for current state. Instruction observations may describe -/// mutations, but must not emit a second competing metadata-state projection. -pub fn materializer_metadata_materialize_metaplex_metadata_snapshot( - snapshot: &crate::DcMetadataMtmCanonicalMetadataAccountSnapshot, -) -> std::result::Result { - if !snapshot.provenance.committed { - return std::result::Result::Err( - "Metaplex metadata state materialization requires committed provenance".to_string(), - ); - } - if snapshot.identity.materialization - != crate::DcMetadataMtmCanonicalMaterializationPolicy::ActiveState - { - return std::result::Result::Err( - "Metaplex MetadataV1 materialization requires active-state policy".to_string(), - ); - } - let metadata = match &snapshot.state { - crate::DcMetadataMtmCanonicalMetadataAccountState::Metadata(value) => value, - _ => { - return std::result::Result::Err( - "Metaplex metadata materializer received another account layout".to_string(), - ); - }, - }; - let slot = snapshot.provenance.slot.map(|value| return value.to_string()); - let write_version = snapshot.provenance.write_version.map(|value| return value.to_string()); - let payload = serde_json::json!({ - "projectionVersion": 1, - "domain": "metaplex_token_metadata", - "projectionSemantics": "authoritative_committed_account_snapshot", - "stableId": snapshot.identity.stable_id, - "account": metadata.account, - "mint": metadata.mint, - "updateAuthority": metadata.update_authority, - "name": metadata.name, - "symbol": metadata.symbol, - "uri": metadata.uri, - "sellerFeeBasisPoints": metadata.seller_fee_basis_points, - "creators": metadata.payload_json.get("creators").cloned().unwrap_or(serde_json::Value::Null), - "collection": metadata.payload_json.get("collection").cloned().unwrap_or(serde_json::Value::Null), - "uses": metadata.payload_json.get("uses").cloned().unwrap_or(serde_json::Value::Null), - "tokenStandard": metadata.payload_json.get("tokenStandard").cloned().unwrap_or(serde_json::Value::Null), - "programmableConfig": metadata.payload_json.get("programmableConfig").cloned().unwrap_or(serde_json::Value::Null), - "primarySaleHappened": metadata.primary_sale_happened, - "isMutable": metadata.is_mutable, - "layout": snapshot.identity.layout, - "layoutDiscriminant": snapshot.identity.discriminant, - "canonicalVersion": snapshot.identity.canonical_version, - "lifecycle": "active", - "materializationPolicy": "active_state", - "metadataSource": "metaplex_owned_account", - "accountState": metadata.payload_json, - "externalUriFetched": false, - "stateAuthority": "owned_account_snapshot", - "instructionProjectionRole": "mutation_observation_only_no_duplicate_state", - "provenance": { - "source": format!("{:?}", snapshot.provenance.source), - "slot": slot, - "writeVersion": write_version, - "signature": snapshot.provenance.signature, - "instructionIndex": snapshot.provenance.instruction_index, - "innerInstructionIndex": snapshot.provenance.inner_instruction_index, - "committed": snapshot.provenance.committed, - "processorName": "kb-lib.materializer.metadata", - "processorVersion": env!("CARGO_PKG_VERSION") - } - }); - return std::result::Result::Ok(crate::MtApiMaterializedOutput { - output_key: format!("metaplex_metadata_state:{}", metadata.account), - family: crate::MdMaterializedEventFamily::Metadata, - payload_json: payload, - }); -} - -#[cfg(test)] -mod tests_metaplex_metadata_state { - pub(super) fn snapshot( - committed: bool, - ) -> crate::DcMetadataMtmCanonicalMetadataAccountSnapshot { - let metadata = crate::DcMetadataMtmMetadataAccountSnapshot { - account: "metadata111".to_string(), - bump: 1, - mint: "mint111".to_string(), - update_authority: "authority111".to_string(), - name: "Asset".to_string(), - symbol: "AST".to_string(), - uri: "https://example.invalid/asset.json".to_string(), - seller_fee_basis_points: 500, - primary_sale_happened: true, - is_mutable: false, - payload_json: serde_json::json!({ - "creators": [{"address":"creator111","verified":true,"share":100}], - "collection": {"verified":true,"key":"collection111"}, - "uses": {"useMethod":"Single","remaining":"0","total":"1"}, - "tokenStandard": "ProgrammableNonFungible", - "programmableConfig": {"ruleSet":"rules111"} - }), - }; - return crate::DcMetadataMtmCanonicalMetadataAccountSnapshot::from_metadata( - metadata, - crate::DcMetadataMtmCanonicalAccountProvenance { - source: crate::DcMetadataMtmCanonicalObservationSource::Backfill, - slot: std::option::Option::Some(42), - write_version: std::option::Option::Some(7), - signature: std::option::Option::Some("signature111".to_string()), - instruction_index: std::option::Option::Some(3), - inner_instruction_index: std::option::Option::None, - committed, - }, - ); - } - - #[test] - fn account_snapshot_is_one_stable_authoritative_metadata_projection() { - let first = - crate::materializer_metadata_materialize_metaplex_metadata_snapshot(&snapshot(true)); - let second = - crate::materializer_metadata_materialize_metaplex_metadata_snapshot(&snapshot(true)); - assert_eq!(first, second); - assert_eq!( - first.as_ref().map(|value| return value.output_key.as_str()), - std::result::Result::Ok("metaplex_metadata_state:metadata111") - ); - assert_eq!( - first.as_ref().map(|value| return value.payload_json["metadataSource"].clone()), - std::result::Result::Ok(serde_json::json!("metaplex_owned_account")) - ); - assert_eq!( - first.as_ref().map(|value| return value.payload_json["creators"].clone()), - std::result::Result::Ok( - serde_json::json!([{"address":"creator111","verified":true,"share":100}]) - ) - ); - assert_eq!( - first - .as_ref() - .map(|value| return value.payload_json["programmableConfig"].clone()), - std::result::Result::Ok(serde_json::json!({"ruleSet":"rules111"})) - ); - assert_eq!( - first - .as_ref() - .map(|value| return value.payload_json["instructionProjectionRole"].clone()), - std::result::Result::Ok(serde_json::json!( - "mutation_observation_only_no_duplicate_state" - )) - ); - assert_eq!( - first - .as_ref() - .map(|value| return value.payload_json["accountState"]["creators"][0]["share"] - .clone()), - std::result::Result::Ok(serde_json::json!(100)) - ); - } - - #[test] - fn uncommitted_or_non_metadata_state_is_rejected() { - assert!( - crate::materializer_metadata_materialize_metaplex_metadata_snapshot(&snapshot(false)) - .is_err() - ); - let mut wrong = snapshot(true); - wrong.state = crate::DcMetadataMtmCanonicalMetadataAccountState::EditionMarker( - crate::DcMetadataMtmEditionMarkerAccountSnapshot { - account: "marker111".to_string(), - bump: 1, - mint: "mint111".to_string(), - kind: crate::DcMetadataMtmEditionMarkerAccountKind::EditionMarkerV1, - marker_group: std::option::Option::Some(0), - edition: 1, - byte_index: 0, - bit_mask: 64, - edition_taken: true, - ledger: std::vec![64], - payload_json: serde_json::json!({}), - }, - ); - assert!( - crate::materializer_metadata_materialize_metaplex_metadata_snapshot(&wrong).is_err() - ); - } -} - -/// Materialize any canonical Metaplex account state other than `MetadataV1`. -/// -/// Every account address maps to one stable state key. Historical layouts remain materializable -/// with an explicit historical lifecycle, while execution policy remains independently disabled. -pub fn materializer_metadata_materialize_metaplex_account_snapshot( - snapshot: &crate::DcMetadataMtmCanonicalMetadataAccountSnapshot, -) -> std::result::Result { - if !snapshot.provenance.committed { - return std::result::Result::Err( - "Metaplex account-state materialization requires committed provenance".to_string(), - ); - } - let (projection_kind, fact_owner, state_json) = match &snapshot.state { - crate::DcMetadataMtmCanonicalMetadataAccountState::Metadata(_) => { - return std::result::Result::Err( - "MetadataV1 must use the dedicated metadata-state materializer".to_string(), - ); - }, - crate::DcMetadataMtmCanonicalMetadataAccountState::Edition(value) => { - ("edition_state", "kb-lib.materializer.metadata", value.payload_json.clone()) - }, - crate::DcMetadataMtmCanonicalMetadataAccountState::EditionMarker(value) => ( - "edition_marker_state", - "kb-lib.materializer.metadata", - value.payload_json.clone(), - ), - crate::DcMetadataMtmCanonicalMetadataAccountState::TokenRecord(value) => ( - "programmable_token_state", - "kb-lib.materializer.lifecycle", - value.payload_json.clone(), - ), - crate::DcMetadataMtmCanonicalMetadataAccountState::DelegateRecord(value) => { - ("delegate_admin_state", "kb-lib.materializer.admin", value.payload_json.clone()) - }, - crate::DcMetadataMtmCanonicalMetadataAccountState::AuthorityRecord(value) => { - ("authority_admin_state", "kb-lib.materializer.admin", value.payload_json.clone()) - }, - crate::DcMetadataMtmCanonicalMetadataAccountState::TokenOwnedEscrow(value) => ( - "token_owned_escrow_state", - "kb-lib.materializer.lifecycle", - value.payload_json.clone(), - ), - crate::DcMetadataMtmCanonicalMetadataAccountState::ReservationList(value) => ( - "historical_reservation_state", - "kb-lib.materializer.metadata", - value.payload_json.clone(), - ), - }; - let lifecycle = match snapshot.identity.lifecycle { - crate::DcMetadataMtmCanonicalAccountLifecycle::Active => "active", - crate::DcMetadataMtmCanonicalAccountLifecycle::ActiveExperimental => "active_experimental", - crate::DcMetadataMtmCanonicalAccountLifecycle::Historical => "historical", - }; - let materialization_policy = match snapshot.identity.materialization { - crate::DcMetadataMtmCanonicalMaterializationPolicy::ActiveState => "active_state", - crate::DcMetadataMtmCanonicalMaterializationPolicy::HistoricalState => "historical_state", - }; - let execution_policy = match snapshot.identity.execution { - crate::DcMetadataMtmCanonicalExecutionPolicy::CurrentCandidate => "current_candidate", - crate::DcMetadataMtmCanonicalExecutionPolicy::ExperimentalCandidate => { - "experimental_candidate" - }, - crate::DcMetadataMtmCanonicalExecutionPolicy::NeverHistorical => "never_historical", - }; - return std::result::Result::Ok(crate::MtApiMaterializedOutput { - output_key: format!("metaplex_account_state:{}", snapshot.identity.account), - family: crate::MdMaterializedEventFamily::Metadata, - payload_json: serde_json::json!({ - "projectionVersion": 1, - "domain": "metaplex_token_metadata", - "projectionKind": projection_kind, - "projectionSemantics": "authoritative_committed_owned_account_snapshot", - "stableId": snapshot.identity.stable_id, - "account": snapshot.identity.account, - "programId": snapshot.identity.program_id, - "layout": snapshot.identity.layout, - "layoutDiscriminant": snapshot.identity.discriminant, - "canonicalVersion": snapshot.identity.canonical_version, - "lifecycle": lifecycle, - "materializationPolicy": materialization_policy, - "executionPolicy": execution_policy, - "factOwner": fact_owner, - "metadataSource": "metaplex_owned_account", - "accountState": state_json, - "instructionProjectionRole": "mutation_observation_only_no_duplicate_state", - "provenance": { - "source": format!("{:?}", snapshot.provenance.source), - "slot": snapshot.provenance.slot.map(|value: u64| return value.to_string()), - "writeVersion": snapshot.provenance.write_version.map(|value: u64| return value.to_string()), - "signature": snapshot.provenance.signature, - "instructionIndex": snapshot.provenance.instruction_index, - "innerInstructionIndex": snapshot.provenance.inner_instruction_index, - "committed": snapshot.provenance.committed, - "processorName": "kb-lib.materializer.metadata", - "processorVersion": env!("CARGO_PKG_VERSION") - } - }), - }); -} - -#[cfg(test)] -mod tests_metaplex_account_state { - fn provenance() -> crate::DcMetadataMtmCanonicalAccountProvenance { - return crate::DcMetadataMtmCanonicalAccountProvenance { - source: crate::DcMetadataMtmCanonicalObservationSource::Backfill, - slot: std::option::Option::Some(90), - write_version: std::option::Option::Some(2), - signature: std::option::Option::None, - instruction_index: std::option::Option::None, - inner_instruction_index: std::option::Option::None, - committed: true, - }; - } - - #[test] - fn historical_edition_and_reservation_are_stable_materializable_state() { - let edition = crate::DcMetadataMtmCanonicalMetadataAccountSnapshot::from_edition( - crate::DcMetadataMtmEditionAccountSnapshot { - account: "master-v1".to_string(), - bump: 1, - mint: "mint-v1".to_string(), - kind: crate::DcMetadataMtmEditionAccountKind::MasterEditionV1, - supply: std::option::Option::Some(3), - max_supply: std::option::Option::Some(10), - parent: std::option::Option::None, - edition: std::option::Option::None, - printing_mint: std::option::Option::Some("printing".to_string()), - one_time_printing_authorization_mint: std::option::Option::Some( - "authorization".to_string(), - ), - payload_json: serde_json::json!({"supply":"3","maxSupply":"10"}), - }, - provenance(), - ); - let reservation = - crate::DcMetadataMtmCanonicalMetadataAccountSnapshot::from_reservation_list( - crate::DcMetadataMtmReservationListAccountSnapshot { - account: "reservation-v2".to_string(), - bump: 2, - kind: crate::DcMetadataMtmReservationListAccountKind::V2, - master_edition: "master-v1".to_string(), - resource: "resource".to_string(), - supply_snapshot: std::option::Option::Some(3), - reservations: std::vec::Vec::new(), - total_reservation_spots: 0, - current_reservation_spots: 0, - payload_json: serde_json::json!({"reservations":[]}), - }, - provenance(), - ); - let first = crate::materializer_metadata_materialize_metaplex_account_snapshot(&edition); - let second = - crate::materializer_metadata_materialize_metaplex_account_snapshot(&reservation); - assert_eq!( - first.as_ref().map(|value| return value.payload_json["lifecycle"].clone()), - std::result::Result::Ok(serde_json::json!("historical")) - ); - assert_eq!( - first.as_ref().map(|value| return value.payload_json["executionPolicy"].clone()), - std::result::Result::Ok(serde_json::json!("never_historical")) - ); - assert_eq!( - second.as_ref().map(|value| return value.output_key.as_str()), - std::result::Result::Ok("metaplex_account_state:reservation-v2") - ); - } - - #[test] - fn programmable_delegate_authority_and_escrow_keep_unique_fact_owners() { - let token = crate::DcMetadataMtmCanonicalMetadataAccountSnapshot::from_token_record( - crate::DcMetadataMtmTokenRecordAccountSnapshot { - account: "token-record".to_string(), - bump: 1, - mint: "mint".to_string(), - token: "token".to_string(), - stored_bump: 1, - state: "Locked".to_string(), - rule_set_revision: std::option::Option::Some(4), - delegate: std::option::Option::Some("delegate".to_string()), - delegate_role: std::option::Option::Some("Transfer".to_string()), - locked_transfer: std::option::Option::Some("destination".to_string()), - locked: true, - payload_json: serde_json::json!({"state":"Locked"}), - }, - provenance(), - ); - let output = crate::materializer_metadata_materialize_metaplex_account_snapshot(&token); - assert_eq!( - output.as_ref().map(|value| return value.payload_json["factOwner"].clone()), - std::result::Result::Ok(serde_json::json!("kb-lib.materializer.lifecycle")) - ); - assert_eq!( - output.as_ref().map(|value| return value.payload_json["projectionKind"].clone()), - std::result::Result::Ok(serde_json::json!("programmable_token_state")) - ); - } - - #[test] - fn uncommitted_and_metadata_snapshots_use_explicit_boundaries() { - let mut marker = crate::DcMetadataMtmCanonicalMetadataAccountSnapshot::from_edition_marker( - crate::DcMetadataMtmEditionMarkerAccountSnapshot { - account: "marker".to_string(), - bump: 1, - mint: "mint".to_string(), - kind: crate::DcMetadataMtmEditionMarkerAccountKind::EditionMarkerV2, - marker_group: std::option::Option::None, - edition: 8, - byte_index: 1, - bit_mask: 128, - edition_taken: true, - ledger: std::vec![0, 128], - payload_json: serde_json::json!({"editionTaken":true}), - }, - provenance(), - ); - marker.provenance.committed = false; - assert!( - crate::materializer_metadata_materialize_metaplex_account_snapshot(&marker).is_err() - ); - assert!( - crate::materializer_metadata_materialize_metaplex_account_snapshot( - &super::tests_metaplex_metadata_state::snapshot(true) - ) - .is_err() - ); - } -} - -#[cfg(test)] -mod tests_token_2022_state { - #[test] - fn embedded_metadata_group_and_member_have_distinct_stable_outputs() { - let state = crate::DcToken2022State { - kind: crate::DcToken2022StateKind::Mint, - base_fields: serde_json::json!({"supply":"1","decimals":0}), - base_hex: "00".repeat(82), - account_type: std::option::Option::Some(1), - extensions: std::vec![ - crate::DcToken2022TlvEntry { - extension_type: 19, - extension_name: "token_metadata", - value_hex: "01".to_string(), - value_fields: serde_json::json!({"name":"Token","symbol":"TOK","uri":"https://example.invalid/token.json"}), - }, - crate::DcToken2022TlvEntry { - extension_type: 20, - extension_name: "token_group", - value_hex: "02".to_string(), - value_fields: serde_json::json!({"mint":"mint111","size":"1","maxSize":"10"}), - }, - crate::DcToken2022TlvEntry { - extension_type: 21, - extension_name: "token_group_member", - value_hex: "03".to_string(), - value_fields: serde_json::json!({"mint":"mint111","group":"group111","memberNumber":"1"}), - }, - ], - }; - let outputs = - crate::materializer_metadata_materialize_token_2022_snapshot("mint111", 9, &state); - assert_eq!(outputs.as_ref().map(|values| return values.len()), std::result::Result::Ok(3)); - assert_eq!( - outputs - .as_ref() - .map(|values| return values[0].payload_json["metadataSource"].clone()), - std::result::Result::Ok(serde_json::json!("token_2022_embedded_tlv")) - ); - assert_eq!( - outputs - .as_ref() - .map(|values| return values[0].payload_json["externalUriFetched"].clone()), - std::result::Result::Ok(serde_json::json!(false)) - ); - assert_eq!( - outputs - .as_ref() - .map(|values| return values[1].payload_json["projectionKind"].clone()), - std::result::Result::Ok(serde_json::json!("token_group")) - ); - } - - #[test] - fn snapshot_is_idempotent_filters_unrelated_extensions_and_requires_mint_identity() { - let state = crate::DcToken2022State { - kind: crate::DcToken2022StateKind::Mint, - base_fields: serde_json::json!({}), - base_hex: "00".repeat(82), - account_type: std::option::Option::Some(1), - extensions: std::vec![crate::DcToken2022TlvEntry { - extension_type: 1, - extension_name: "transfer_fee_config", - value_hex: "00".to_string(), - value_fields: serde_json::json!({}), - }], - }; - let first = - crate::materializer_metadata_materialize_token_2022_snapshot("mint111", 9, &state); - let second = - crate::materializer_metadata_materialize_token_2022_snapshot("mint111", 9, &state); - assert_eq!(first, second); - assert_eq!(first.as_ref().map(|values| return values.len()), std::result::Result::Ok(0)); - assert!( - crate::materializer_metadata_materialize_token_2022_snapshot("", 9, &state).is_err() - ); - let account_state = crate::DcToken2022State { - kind: crate::DcToken2022StateKind::Account, - base_fields: serde_json::json!({}), - base_hex: "00".repeat(165), - account_type: std::option::Option::None, - extensions: std::vec::Vec::new(), - }; - assert!( - crate::materializer_metadata_materialize_token_2022_snapshot( - "account111", - 9, - &account_state - ) - .is_err() - ); - } -} diff --git a/kb-lib/src/materializer/metadata/metaplex_token_metadata.rs b/kb-lib/src/materializer/metadata/metaplex_token_metadata.rs new file mode 100644 index 0000000..ca65bc3 --- /dev/null +++ b/kb-lib/src/materializer/metadata/metaplex_token_metadata.rs @@ -0,0 +1,22 @@ +// file: kb-lib/src/materializer/metadata/metaplex_token_metadata.rs +// version: 2 + +//! Metaplex Token Metadata account-state materializer. + +mod account; +mod constants; +mod state; + +/// Materializes one canonical non-Metadata Metaplex account snapshot. +pub use self::account::materializer_metadata_materialize_metaplex_account_snapshot; +/// Materializes one canonical Metaplex Metadata account snapshot. +pub use self::state::materializer_metadata_materialize_metaplex_metadata_snapshot; + +/// Stable runtime component name for Metaplex Token Metadata projections. +pub(crate) use self::constants::MT_METADATA_METAPLEX_TOKEN_METADATA_COMPONENT_NAME; +/// Stable processor name for Metaplex Token Metadata projections. +pub(crate) use self::constants::MT_METADATA_METAPLEX_TOKEN_METADATA_PROCESSOR_NAME; +/// Stable Metaplex Token Metadata projection version. +pub(crate) use self::constants::MT_METADATA_METAPLEX_TOKEN_METADATA_PROJECTION_VERSION; +/// Canonical tracing target for Metaplex Token Metadata materialization. +pub(crate) use self::constants::TRACING_TARGET_MATERIALIZER_METADATA_METAPLEX_TOKEN_METADATA; diff --git a/kb-lib/src/materializer/metadata/metaplex_token_metadata/account.rs b/kb-lib/src/materializer/metadata/metaplex_token_metadata/account.rs new file mode 100644 index 0000000..f368a31 --- /dev/null +++ b/kb-lib/src/materializer/metadata/metaplex_token_metadata/account.rs @@ -0,0 +1,283 @@ +// file: kb-lib/src/materializer/metadata/metaplex_token_metadata/account.rs +// version: 5 + +//! Authoritative non-Metadata Metaplex account-state materialization. + +/// Materialize any canonical Metaplex account state other than `MetadataV1`. +/// +/// Every account address maps to one stable state key. Historical layouts remain materializable +/// with an explicit historical lifecycle, while execution policy remains independently disabled. +pub fn materializer_metadata_materialize_metaplex_account_snapshot( + snapshot: &crate::DcMetadataMtmCanonicalMetadataAccountSnapshot, +) -> std::result::Result { + if !snapshot.provenance.committed { + tracing::debug!( + target: crate::TRACING_TARGET_MATERIALIZER_METADATA_METAPLEX_TOKEN_METADATA, + action = "materialize_metaplex_account_snapshot", + component_name = crate::MT_METADATA_METAPLEX_TOKEN_METADATA_COMPONENT_NAME, + processor_name = crate::MT_METADATA_METAPLEX_TOKEN_METADATA_PROCESSOR_NAME, + processor_version = env!("CARGO_PKG_VERSION"), + account = %snapshot.identity.account.as_str(), + committed = false, + status = "refused", + error_code = "uncommitted_metaplex_account_snapshot", + "refuse uncommitted Metaplex account snapshot" + ); + return std::result::Result::Err( + "Metaplex account-state materialization requires committed provenance".to_string(), + ); + } + let (projection_kind, fact_owner, state_json) = match &snapshot.state { + crate::DcMetadataMtmCanonicalMetadataAccountState::Metadata(_) => { + return std::result::Result::Err( + "MetadataV1 must use the dedicated metadata-state materializer".to_string(), + ); + }, + crate::DcMetadataMtmCanonicalMetadataAccountState::Edition(value) => ( + "edition_state", + crate::MT_METADATA_METAPLEX_TOKEN_METADATA_COMPONENT_NAME, + value.payload_json.clone(), + ), + crate::DcMetadataMtmCanonicalMetadataAccountState::EditionMarker(value) => ( + "edition_marker_state", + crate::MT_METADATA_METAPLEX_TOKEN_METADATA_COMPONENT_NAME, + value.payload_json.clone(), + ), + crate::DcMetadataMtmCanonicalMetadataAccountState::TokenRecord(value) => ( + "programmable_token_state", + crate::MT_LIFECYCLE_COMPONENT_NAME, + value.payload_json.clone(), + ), + crate::DcMetadataMtmCanonicalMetadataAccountState::DelegateRecord(value) => ( + "delegate_admin_state", + crate::MT_ADMIN_COMPONENT_NAME, + value.payload_json.clone(), + ), + crate::DcMetadataMtmCanonicalMetadataAccountState::AuthorityRecord(value) => ( + "authority_admin_state", + crate::MT_ADMIN_COMPONENT_NAME, + value.payload_json.clone(), + ), + crate::DcMetadataMtmCanonicalMetadataAccountState::TokenOwnedEscrow(value) => ( + "token_owned_escrow_state", + crate::MT_LIFECYCLE_COMPONENT_NAME, + value.payload_json.clone(), + ), + crate::DcMetadataMtmCanonicalMetadataAccountState::ReservationList(value) => ( + "historical_reservation_state", + crate::MT_METADATA_METAPLEX_TOKEN_METADATA_COMPONENT_NAME, + value.payload_json.clone(), + ), + }; + let lifecycle = match snapshot.identity.lifecycle { + crate::DcMetadataMtmCanonicalAccountLifecycle::Active => "active", + crate::DcMetadataMtmCanonicalAccountLifecycle::ActiveExperimental => "active_experimental", + crate::DcMetadataMtmCanonicalAccountLifecycle::Historical => "historical", + }; + let materialization_policy = match snapshot.identity.materialization { + crate::DcMetadataMtmCanonicalMaterializationPolicy::ActiveState => "active_state", + crate::DcMetadataMtmCanonicalMaterializationPolicy::HistoricalState => "historical_state", + }; + let execution_policy = match snapshot.identity.execution { + crate::DcMetadataMtmCanonicalExecutionPolicy::CurrentCandidate => "current_candidate", + crate::DcMetadataMtmCanonicalExecutionPolicy::ExperimentalCandidate => { + "experimental_candidate" + }, + crate::DcMetadataMtmCanonicalExecutionPolicy::NeverHistorical => "never_historical", + }; + let output = crate::MtApiMaterializedOutput { + output_key: format!("metaplex_account_state:{}", snapshot.identity.account), + family: crate::MdMaterializedEventFamily::Metadata, + payload_json: serde_json::json!({ + "projectionVersion": crate::MT_METADATA_METAPLEX_TOKEN_METADATA_PROJECTION_VERSION, + "domain": "metaplex_token_metadata", + "projectionKind": projection_kind, + "projectionSemantics": "authoritative_committed_owned_account_snapshot", + "stableId": snapshot.identity.stable_id, + "account": snapshot.identity.account, + "programId": snapshot.identity.program_id, + "layout": snapshot.identity.layout, + "layoutDiscriminant": snapshot.identity.discriminant, + "canonicalVersion": snapshot.identity.canonical_version, + "lifecycle": lifecycle, + "materializationPolicy": materialization_policy, + "executionPolicy": execution_policy, + "factOwner": fact_owner, + "metadataSource": "metaplex_owned_account", + "accountState": state_json, + "instructionProjectionRole": "mutation_observation_only_no_duplicate_state", + "provenance": { + "source": format!("{:?}", snapshot.provenance.source), + "slot": snapshot.provenance.slot.map(|value: u64| return value.to_string()), + "writeVersion": snapshot.provenance.write_version.map(|value: u64| return value.to_string()), + "signature": snapshot.provenance.signature, + "instructionIndex": snapshot.provenance.instruction_index, + "innerInstructionIndex": snapshot.provenance.inner_instruction_index, + "committed": snapshot.provenance.committed, + "processorName": crate::MT_METADATA_METAPLEX_TOKEN_METADATA_PROCESSOR_NAME, + "processorVersion": env!("CARGO_PKG_VERSION") + } + }), + }; + tracing::debug!( + target: crate::TRACING_TARGET_MATERIALIZER_METADATA_METAPLEX_TOKEN_METADATA, + action = "materialize_metaplex_account_snapshot", + component_name = crate::MT_METADATA_METAPLEX_TOKEN_METADATA_COMPONENT_NAME, + processor_name = crate::MT_METADATA_METAPLEX_TOKEN_METADATA_PROCESSOR_NAME, + processor_version = env!("CARGO_PKG_VERSION"), + account = %snapshot.identity.account.as_str(), + projection_kind, + fact_owner, + slot = ?snapshot.provenance.slot, + status = "inserted", + "materialize committed Metaplex account snapshot" + ); + return std::result::Result::Ok(output); +} + +#[cfg(test)] +mod tests_metaplex_account_state { + fn metadata_snapshot() -> crate::DcMetadataMtmCanonicalMetadataAccountSnapshot { + return crate::DcMetadataMtmCanonicalMetadataAccountSnapshot::from_metadata( + crate::DcMetadataMtmMetadataAccountSnapshot { + account: "metadata-boundary".to_string(), + bump: 1, + mint: "mint-boundary".to_string(), + update_authority: "authority-boundary".to_string(), + name: "Boundary".to_string(), + symbol: "BND".to_string(), + uri: "https://example.invalid/boundary.json".to_string(), + seller_fee_basis_points: 0, + primary_sale_happened: false, + is_mutable: true, + payload_json: serde_json::json!({}), + }, + provenance(), + ); + } + + fn provenance() -> crate::DcMetadataMtmCanonicalAccountProvenance { + return crate::DcMetadataMtmCanonicalAccountProvenance { + source: crate::DcMetadataMtmCanonicalObservationSource::Backfill, + slot: std::option::Option::Some(90), + write_version: std::option::Option::Some(2), + signature: std::option::Option::None, + instruction_index: std::option::Option::None, + inner_instruction_index: std::option::Option::None, + committed: true, + }; + } + + #[test] + fn historical_edition_and_reservation_are_stable_materializable_state() { + let edition = crate::DcMetadataMtmCanonicalMetadataAccountSnapshot::from_edition( + crate::DcMetadataMtmEditionAccountSnapshot { + account: "master-v1".to_string(), + bump: 1, + mint: "mint-v1".to_string(), + kind: crate::DcMetadataMtmEditionAccountKind::MasterEditionV1, + supply: std::option::Option::Some(3), + max_supply: std::option::Option::Some(10), + parent: std::option::Option::None, + edition: std::option::Option::None, + printing_mint: std::option::Option::Some("printing".to_string()), + one_time_printing_authorization_mint: std::option::Option::Some( + "authorization".to_string(), + ), + payload_json: serde_json::json!({"supply":"3","maxSupply":"10"}), + }, + provenance(), + ); + let reservation = + crate::DcMetadataMtmCanonicalMetadataAccountSnapshot::from_reservation_list( + crate::DcMetadataMtmReservationListAccountSnapshot { + account: "reservation-v2".to_string(), + bump: 2, + kind: crate::DcMetadataMtmReservationListAccountKind::V2, + master_edition: "master-v1".to_string(), + resource: "resource".to_string(), + supply_snapshot: std::option::Option::Some(3), + reservations: std::vec::Vec::new(), + total_reservation_spots: 0, + current_reservation_spots: 0, + payload_json: serde_json::json!({"reservations":[]}), + }, + provenance(), + ); + let first = crate::materializer_metadata_materialize_metaplex_account_snapshot(&edition); + let second = + crate::materializer_metadata_materialize_metaplex_account_snapshot(&reservation); + assert_eq!( + first.as_ref().map(|value| return value.payload_json["lifecycle"].clone()), + std::result::Result::Ok(serde_json::json!("historical")) + ); + assert_eq!( + first.as_ref().map(|value| return value.payload_json["executionPolicy"].clone()), + std::result::Result::Ok(serde_json::json!("never_historical")) + ); + assert_eq!( + second.as_ref().map(|value| return value.output_key.as_str()), + std::result::Result::Ok("metaplex_account_state:reservation-v2") + ); + } + + #[test] + fn programmable_delegate_authority_and_escrow_keep_unique_fact_owners() { + let token = crate::DcMetadataMtmCanonicalMetadataAccountSnapshot::from_token_record( + crate::DcMetadataMtmTokenRecordAccountSnapshot { + account: "token-record".to_string(), + bump: 1, + mint: "mint".to_string(), + token: "token".to_string(), + stored_bump: 1, + state: "Locked".to_string(), + rule_set_revision: std::option::Option::Some(4), + delegate: std::option::Option::Some("delegate".to_string()), + delegate_role: std::option::Option::Some("Transfer".to_string()), + locked_transfer: std::option::Option::Some("destination".to_string()), + locked: true, + payload_json: serde_json::json!({"state":"Locked"}), + }, + provenance(), + ); + let output = crate::materializer_metadata_materialize_metaplex_account_snapshot(&token); + assert_eq!( + output.as_ref().map(|value| return value.payload_json["factOwner"].clone()), + std::result::Result::Ok(serde_json::json!(crate::MT_LIFECYCLE_COMPONENT_NAME)) + ); + assert_eq!( + output.as_ref().map(|value| return value.payload_json["projectionKind"].clone()), + std::result::Result::Ok(serde_json::json!("programmable_token_state")) + ); + } + + #[test] + fn uncommitted_and_metadata_snapshots_use_explicit_boundaries() { + let mut marker = crate::DcMetadataMtmCanonicalMetadataAccountSnapshot::from_edition_marker( + crate::DcMetadataMtmEditionMarkerAccountSnapshot { + account: "marker".to_string(), + bump: 1, + mint: "mint".to_string(), + kind: crate::DcMetadataMtmEditionMarkerAccountKind::EditionMarkerV2, + marker_group: std::option::Option::None, + edition: 8, + byte_index: 1, + bit_mask: 128, + edition_taken: true, + ledger: std::vec![0, 128], + payload_json: serde_json::json!({"editionTaken":true}), + }, + provenance(), + ); + marker.provenance.committed = false; + assert!( + crate::materializer_metadata_materialize_metaplex_account_snapshot(&marker).is_err() + ); + assert!( + crate::materializer_metadata_materialize_metaplex_account_snapshot( + &metadata_snapshot() + ) + .is_err() + ); + } +} diff --git a/kb-lib/src/materializer/metadata/metaplex_token_metadata/constants.rs b/kb-lib/src/materializer/metadata/metaplex_token_metadata/constants.rs new file mode 100644 index 0000000..31c619a --- /dev/null +++ b/kb-lib/src/materializer/metadata/metaplex_token_metadata/constants.rs @@ -0,0 +1,16 @@ +// file: kb-lib/src/materializer/metadata/metaplex_token_metadata/constants.rs +// version: 4 + +//! Local constants for the metadata metaplex_token_metadata materializer. + +/// Stable runtime component name for the metadata metaplex_token_metadata materializer. +pub(crate) const MT_METADATA_METAPLEX_TOKEN_METADATA_COMPONENT_NAME: &str = + "kb-lib.materializer.metadata.metaplex_token_metadata"; +/// Stable processor name persisted by metadata projections. +pub(crate) const MT_METADATA_METAPLEX_TOKEN_METADATA_PROCESSOR_NAME: &str = + "materializer.metadata.metaplex_token_metadata"; +/// Stable projection contract version. +pub(crate) const MT_METADATA_METAPLEX_TOKEN_METADATA_PROJECTION_VERSION: u32 = 2; +/// Canonical tracing target for the metadata metaplex_token_metadata materializer. +pub(crate) const TRACING_TARGET_MATERIALIZER_METADATA_METAPLEX_TOKEN_METADATA: &str = + "kb-lib.materializer.metadata.metaplex_token_metadata"; diff --git a/kb-lib/src/materializer/metadata/metaplex_token_metadata/state.rs b/kb-lib/src/materializer/metadata/metaplex_token_metadata/state.rs new file mode 100644 index 0000000..61f0573 --- /dev/null +++ b/kb-lib/src/materializer/metadata/metaplex_token_metadata/state.rs @@ -0,0 +1,229 @@ +// file: kb-lib/src/materializer/metadata/metaplex_token_metadata/state.rs +// version: 5 + +//! Authoritative Metaplex MetadataV1 state materialization. + +/// Materialize one canonical Metaplex `MetadataV1` account snapshot. +/// +/// Account snapshots are authoritative for current state. Instruction observations may describe +/// mutations, but must not emit a second competing metadata-state projection. +pub fn materializer_metadata_materialize_metaplex_metadata_snapshot( + snapshot: &crate::DcMetadataMtmCanonicalMetadataAccountSnapshot, +) -> std::result::Result { + if !snapshot.provenance.committed { + tracing::debug!( + target: crate::TRACING_TARGET_MATERIALIZER_METADATA_METAPLEX_TOKEN_METADATA, + action = "materialize_metaplex_metadata_snapshot", + component_name = crate::MT_METADATA_METAPLEX_TOKEN_METADATA_COMPONENT_NAME, + processor_name = crate::MT_METADATA_METAPLEX_TOKEN_METADATA_PROCESSOR_NAME, + processor_version = env!("CARGO_PKG_VERSION"), + account = %snapshot.identity.account.as_str(), + committed = false, + status = "refused", + error_code = "uncommitted_metaplex_metadata_snapshot", + "refuse uncommitted Metaplex metadata snapshot" + ); + return std::result::Result::Err( + "Metaplex metadata state materialization requires committed provenance".to_string(), + ); + } + if snapshot.identity.materialization + != crate::DcMetadataMtmCanonicalMaterializationPolicy::ActiveState + { + return std::result::Result::Err( + "Metaplex MetadataV1 materialization requires active-state policy".to_string(), + ); + } + let metadata = match &snapshot.state { + crate::DcMetadataMtmCanonicalMetadataAccountState::Metadata(value) => value, + _ => { + return std::result::Result::Err( + "Metaplex metadata materializer received another account layout".to_string(), + ); + }, + }; + let slot = snapshot.provenance.slot.map(|value| return value.to_string()); + let write_version = snapshot.provenance.write_version.map(|value| return value.to_string()); + let payload = serde_json::json!({ + "projectionVersion": crate::MT_METADATA_METAPLEX_TOKEN_METADATA_PROJECTION_VERSION, + "domain": "metaplex_token_metadata", + "projectionSemantics": "authoritative_committed_account_snapshot", + "stableId": snapshot.identity.stable_id, + "account": metadata.account, + "mint": metadata.mint, + "updateAuthority": metadata.update_authority, + "name": metadata.name, + "symbol": metadata.symbol, + "uri": metadata.uri, + "sellerFeeBasisPoints": metadata.seller_fee_basis_points, + "creators": json_field_or_null(&metadata.payload_json, "creators"), + "collection": json_field_or_null(&metadata.payload_json, "collection"), + "uses": json_field_or_null(&metadata.payload_json, "uses"), + "tokenStandard": json_field_or_null(&metadata.payload_json, "tokenStandard"), + "programmableConfig": json_field_or_null(&metadata.payload_json, "programmableConfig"), + "primarySaleHappened": metadata.primary_sale_happened, + "isMutable": metadata.is_mutable, + "layout": snapshot.identity.layout, + "layoutDiscriminant": snapshot.identity.discriminant, + "canonicalVersion": snapshot.identity.canonical_version, + "lifecycle": "active", + "materializationPolicy": "active_state", + "metadataSource": "metaplex_owned_account", + "accountState": metadata.payload_json, + "externalUriFetched": false, + "stateAuthority": "owned_account_snapshot", + "instructionProjectionRole": "mutation_observation_only_no_duplicate_state", + "provenance": { + "source": format!("{:?}", snapshot.provenance.source), + "slot": slot, + "writeVersion": write_version, + "signature": snapshot.provenance.signature, + "instructionIndex": snapshot.provenance.instruction_index, + "innerInstructionIndex": snapshot.provenance.inner_instruction_index, + "committed": snapshot.provenance.committed, + "processorName": crate::MT_METADATA_METAPLEX_TOKEN_METADATA_PROCESSOR_NAME, + "processorVersion": env!("CARGO_PKG_VERSION") + } + }); + let output = crate::MtApiMaterializedOutput { + output_key: format!("metaplex_metadata_state:{}", metadata.account), + family: crate::MdMaterializedEventFamily::Metadata, + payload_json: payload, + }; + tracing::debug!( + target: crate::TRACING_TARGET_MATERIALIZER_METADATA_METAPLEX_TOKEN_METADATA, + action = "materialize_metaplex_metadata_snapshot", + component_name = crate::MT_METADATA_METAPLEX_TOKEN_METADATA_COMPONENT_NAME, + processor_name = crate::MT_METADATA_METAPLEX_TOKEN_METADATA_PROCESSOR_NAME, + processor_version = env!("CARGO_PKG_VERSION"), + account = %metadata.account.as_str(), + mint = %metadata.mint.as_str(), + slot = ?snapshot.provenance.slot, + status = "inserted", + "materialize committed Metaplex MetadataV1 snapshot" + ); + return std::result::Result::Ok(output); +} + +fn json_field_or_null(value: &serde_json::Value, field: &str) -> serde_json::Value { + return match value.get(field) { + std::option::Option::Some(value) => value.clone(), + std::option::Option::None => serde_json::Value::Null, + }; +} + +#[cfg(test)] +mod tests_metaplex_metadata_state { + fn snapshot(committed: bool) -> crate::DcMetadataMtmCanonicalMetadataAccountSnapshot { + let metadata = crate::DcMetadataMtmMetadataAccountSnapshot { + account: "metadata111".to_string(), + bump: 1, + mint: "mint111".to_string(), + update_authority: "authority111".to_string(), + name: "Asset".to_string(), + symbol: "AST".to_string(), + uri: "https://example.invalid/asset.json".to_string(), + seller_fee_basis_points: 500, + primary_sale_happened: true, + is_mutable: false, + payload_json: serde_json::json!({ + "creators": [{"address":"creator111","verified":true,"share":100}], + "collection": {"verified":true,"key":"collection111"}, + "uses": {"useMethod":"Single","remaining":"0","total":"1"}, + "tokenStandard": "ProgrammableNonFungible", + "programmableConfig": {"ruleSet":"rules111"} + }), + }; + return crate::DcMetadataMtmCanonicalMetadataAccountSnapshot::from_metadata( + metadata, + crate::DcMetadataMtmCanonicalAccountProvenance { + source: crate::DcMetadataMtmCanonicalObservationSource::Backfill, + slot: std::option::Option::Some(42), + write_version: std::option::Option::Some(7), + signature: std::option::Option::Some("signature111".to_string()), + instruction_index: std::option::Option::Some(3), + inner_instruction_index: std::option::Option::None, + committed, + }, + ); + } + + #[test] + fn account_snapshot_is_one_stable_authoritative_metadata_projection() { + let first = + crate::materializer_metadata_materialize_metaplex_metadata_snapshot(&snapshot(true)); + let second = + crate::materializer_metadata_materialize_metaplex_metadata_snapshot(&snapshot(true)); + assert_eq!(first, second); + assert_eq!( + first.as_ref().map(|value| return value.output_key.as_str()), + std::result::Result::Ok("metaplex_metadata_state:metadata111") + ); + assert_eq!( + first.as_ref().map(|value| return value.payload_json["metadataSource"].clone()), + std::result::Result::Ok(serde_json::json!("metaplex_owned_account")) + ); + assert_eq!( + first.as_ref().map(|value| return value.payload_json["creators"].clone()), + std::result::Result::Ok( + serde_json::json!([{"address":"creator111","verified":true,"share":100}]) + ) + ); + assert_eq!( + first + .as_ref() + .map(|value| return value.payload_json["programmableConfig"].clone()), + std::result::Result::Ok(serde_json::json!({"ruleSet":"rules111"})) + ); + assert_eq!( + first + .as_ref() + .map(|value| return value.payload_json["instructionProjectionRole"].clone()), + std::result::Result::Ok(serde_json::json!( + "mutation_observation_only_no_duplicate_state" + )) + ); + assert_eq!( + first.as_ref().map(|value| { + return value.payload_json["provenance"]["processorName"].clone(); + }), + std::result::Result::Ok(serde_json::json!( + crate::MT_METADATA_METAPLEX_TOKEN_METADATA_PROCESSOR_NAME + )) + ); + assert_eq!( + first + .as_ref() + .map(|value| return value.payload_json["accountState"]["creators"][0]["share"] + .clone()), + std::result::Result::Ok(serde_json::json!(100)) + ); + } + + #[test] + fn uncommitted_or_non_metadata_state_is_rejected() { + assert!( + crate::materializer_metadata_materialize_metaplex_metadata_snapshot(&snapshot(false)) + .is_err() + ); + let mut wrong = snapshot(true); + wrong.state = crate::DcMetadataMtmCanonicalMetadataAccountState::EditionMarker( + crate::DcMetadataMtmEditionMarkerAccountSnapshot { + account: "marker111".to_string(), + bump: 1, + mint: "mint111".to_string(), + kind: crate::DcMetadataMtmEditionMarkerAccountKind::EditionMarkerV1, + marker_group: std::option::Option::Some(0), + edition: 1, + byte_index: 0, + bit_mask: 64, + edition_taken: true, + ledger: std::vec![64], + payload_json: serde_json::json!({}), + }, + ); + assert!( + crate::materializer_metadata_materialize_metaplex_metadata_snapshot(&wrong).is_err() + ); + } +} diff --git a/kb-lib/src/materializer/metadata/solana_program_metadata.rs b/kb-lib/src/materializer/metadata/solana_program_metadata.rs index 5dc887e..f434e42 100644 --- a/kb-lib/src/materializer/metadata/solana_program_metadata.rs +++ b/kb-lib/src/materializer/metadata/solana_program_metadata.rs @@ -1,12 +1,16 @@ // file: kb-lib/src/materializer/metadata/solana_program_metadata.rs -// version: 1 +// version: 2 //! Solana Program Metadata materialization surface. mod constants; -mod instruction; +mod materializer; mod state; +/// Compatibility materializer retaining the historical generic metadata type name. +pub use self::materializer::MtMetadataMaterializer; +/// Stable Solana Program Metadata instruction materializer. +pub use self::materializer::MtSolanaProgramMetadataMaterializer; /// Materializes one decoded Solana Program Metadata account snapshot. pub use self::state::materializer_metadata_materialize_solana_program_metadata_account_snapshot; /// Materializes one bounded Solana Program Metadata Buffer account snapshot. @@ -15,10 +19,12 @@ pub use self::state::materializer_metadata_materialize_solana_program_metadata_b pub use self::state::materializer_metadata_materialize_solana_program_metadata_metadata_snapshot; /// Event families accepted by the Solana Program Metadata instruction materializer. -pub(crate) use self::constants::MT_METADATA_SPM_ACCEPTED_FAMILIES; +pub(crate) use self::constants::MT_METADATA_SOLANA_PROGRAM_METADATA_ACCEPTED_FAMILIES; +/// Stable runtime component name for Solana Program Metadata projections. +pub(crate) use self::constants::MT_METADATA_SOLANA_PROGRAM_METADATA_COMPONENT_NAME; /// Stable processor name for Solana Program Metadata projections. -pub(crate) use self::constants::MT_METADATA_SPM_PROCESSOR_NAME; +pub(crate) use self::constants::MT_METADATA_SOLANA_PROGRAM_METADATA_PROCESSOR_NAME; /// Stable projection schema version for Solana Program Metadata outputs. -pub(crate) use self::constants::MT_METADATA_SPM_PROJECTION_VERSION; +pub(crate) use self::constants::MT_METADATA_SOLANA_PROGRAM_METADATA_PROJECTION_VERSION; /// Canonical tracing target for Solana Program Metadata materialization. pub(crate) use self::constants::TRACING_TARGET_MATERIALIZER_METADATA_SOLANA_PROGRAM_METADATA; diff --git a/kb-lib/src/materializer/metadata/solana_program_metadata/constants.rs b/kb-lib/src/materializer/metadata/solana_program_metadata/constants.rs index 28a58e9..372ef30 100644 --- a/kb-lib/src/materializer/metadata/solana_program_metadata/constants.rs +++ b/kb-lib/src/materializer/metadata/solana_program_metadata/constants.rs @@ -1,16 +1,18 @@ // file: kb-lib/src/materializer/metadata/solana_program_metadata/constants.rs -// version: 1 +// version: 4 -//! Solana Program Metadata materializer constants. +//! Local constants for the Solana Program Metadata materializer. -/// Event families accepted by the Solana Program Metadata instruction materializer. -pub(crate) const MT_METADATA_SPM_ACCEPTED_FAMILIES: &[crate::MdEventFamily] = - &[crate::MdEventFamily::Metadata]; -/// Stable processor name for Solana Program Metadata projections. -pub(crate) const MT_METADATA_SPM_PROCESSOR_NAME: &str = +/// Stable runtime component name for Solana Program Metadata materialization. +pub(crate) const MT_METADATA_SOLANA_PROGRAM_METADATA_COMPONENT_NAME: &str = "kb-lib.materializer.metadata.solana_program_metadata"; +/// Stable processor name persisted by Solana Program Metadata projections. +pub(crate) const MT_METADATA_SOLANA_PROGRAM_METADATA_PROCESSOR_NAME: &str = + "materializer.metadata.solana_program_metadata"; /// Stable projection schema version for Solana Program Metadata outputs. -pub(crate) const MT_METADATA_SPM_PROJECTION_VERSION: u16 = 1; +pub(crate) const MT_METADATA_SOLANA_PROGRAM_METADATA_PROJECTION_VERSION: u32 = 1; +pub(crate) const MT_METADATA_SOLANA_PROGRAM_METADATA_ACCEPTED_FAMILIES: &[crate::MdEventFamily] = + &[crate::MdEventFamily::Metadata]; /// Canonical tracing target for Solana Program Metadata materialization. pub(crate) const TRACING_TARGET_MATERIALIZER_METADATA_SOLANA_PROGRAM_METADATA: &str = "kb-lib.materializer.metadata.solana_program_metadata"; diff --git a/kb-lib/src/materializer/metadata/solana_program_metadata/instruction.rs b/kb-lib/src/materializer/metadata/solana_program_metadata/materializer.rs similarity index 71% rename from kb-lib/src/materializer/metadata/solana_program_metadata/instruction.rs rename to kb-lib/src/materializer/metadata/solana_program_metadata/materializer.rs index d0b08b3..a7ed360 100644 --- a/kb-lib/src/materializer/metadata/solana_program_metadata/instruction.rs +++ b/kb-lib/src/materializer/metadata/solana_program_metadata/materializer.rs @@ -1,18 +1,128 @@ -// file: kb-lib/src/materializer/metadata/solana_program_metadata/instruction.rs -// version: 1 +// file: kb-lib/src/materializer/metadata/solana_program_metadata/materializer.rs +// version: 4 //! Committed Solana Program Metadata instruction projections. +/// Stable Solana Program Metadata instruction materializer. +#[derive(Clone, Debug, Default)] +pub struct MtSolanaProgramMetadataMaterializer; + +/// Compatibility materializer retaining the historical generic metadata type name. +#[derive(Clone, Debug, Default)] +pub struct MtMetadataMaterializer; + +impl crate::MtMaterializer for crate::MtSolanaProgramMetadataMaterializer { + fn materializer_name(&self) -> &'static str { + return crate::MT_METADATA_SOLANA_PROGRAM_METADATA_COMPONENT_NAME; + } + + fn materializer_version(&self) -> &'static str { + return env!("CARGO_PKG_VERSION"); + } + + fn accepts_event(&self, event: &crate::MdDecodedProtocolEvent) -> bool { + return event.event_family == crate::MdEventFamily::Metadata + && event.program_id.0 == kb_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID + && event.surface_code.0 == crate::DC_METADATA_SPM_SURFACE_CODE + && crate::DC_METADATA_SPM_INSTRUCTION_ENTRIES + .iter() + .any(|(_, code)| return *code == event.event_name.0.as_str()); + } + + fn materialize_event( + &self, + event: &crate::MdDecodedProtocolEvent, + ) -> kb_core::Result> { + if !crate::MtMaterializer::accepts_event(self, event) { + return std::result::Result::Ok(std::vec::Vec::new()); + } + return std::result::Result::Ok(std::vec![crate::MdMaterializedEvent { + signature: event.signature.clone(), + slot: event.slot, + decoded_family: event.event_family, + materialized_family: crate::MdMaterializedEventFamily::Metadata, + }]); + } +} + +impl crate::MtMaterializer for crate::MtMetadataMaterializer { + fn materializer_name(&self) -> &'static str { + return crate::MT_METADATA_SOLANA_PROGRAM_METADATA_COMPONENT_NAME; + } + + fn materializer_version(&self) -> &'static str { + return env!("CARGO_PKG_VERSION"); + } + + fn accepts_event(&self, event: &crate::MdDecodedProtocolEvent) -> bool { + return crate::MtMaterializer::accepts_event( + &crate::MtSolanaProgramMetadataMaterializer, + event, + ); + } + + fn materialize_event( + &self, + event: &crate::MdDecodedProtocolEvent, + ) -> kb_core::Result> { + return crate::MtMaterializer::materialize_event( + &crate::MtSolanaProgramMetadataMaterializer, + event, + ); + } +} + impl crate::MtApiEventMaterializer for crate::MtMetadataMaterializer { + fn identity(&self) -> crate::MtApiMaterializerIdentity { + return crate::MtApiEventMaterializer::identity( + &crate::MtSolanaProgramMetadataMaterializer, + ); + } + + fn accepted_families(&self) -> &'static [crate::MdEventFamily] { + return crate::MtApiEventMaterializer::accepted_families( + &crate::MtSolanaProgramMetadataMaterializer, + ); + } + + fn accepts_observation(&self, observation: &crate::DcApiDecodedObservation) -> bool { + return crate::MtApiEventMaterializer::accepts_observation( + &crate::MtSolanaProgramMetadataMaterializer, + observation, + ); + } + + fn transaction_policy( + &self, + family: crate::MdEventFamily, + ) -> crate::MtApiMaterializationTransactionPolicy { + return crate::MtApiEventMaterializer::transaction_policy( + &crate::MtSolanaProgramMetadataMaterializer, + family, + ); + } + + fn materialize( + &self, + observation: &crate::DcApiDecodedObservation, + ) -> crate::MtApiMaterializerExecutionResult { + return crate::MtApiEventMaterializer::materialize( + &crate::MtSolanaProgramMetadataMaterializer, + observation, + ); + } +} + +impl crate::MtApiEventMaterializer for crate::MtSolanaProgramMetadataMaterializer { fn identity(&self) -> crate::MtApiMaterializerIdentity { return crate::MtApiMaterializerIdentity { - name: crate::MT_METADATA_SPM_PROCESSOR_NAME.to_string(), + name: crate::MT_METADATA_SOLANA_PROGRAM_METADATA_PROCESSOR_NAME.to_string(), version: env!("CARGO_PKG_VERSION").to_string(), }; } fn accepted_families(&self) -> &'static [crate::MdEventFamily] { - return crate::MT_METADATA_SPM_ACCEPTED_FAMILIES; + return crate::MT_METADATA_SOLANA_PROGRAM_METADATA_ACCEPTED_FAMILIES; } fn accepts_observation(&self, observation: &crate::DcApiDecodedObservation) -> bool { @@ -55,7 +165,7 @@ impl crate::MtApiEventMaterializer for crate::MtMetadataMaterializer { ), family: crate::MdMaterializedEventFamily::Metadata, payload_json: serde_json::json!({ - "projectionVersion": crate::MT_METADATA_SPM_PROJECTION_VERSION, + "projectionVersion": crate::MT_METADATA_SOLANA_PROGRAM_METADATA_PROJECTION_VERSION, "domain": "solana_program_metadata", "projectionKind": "committed_instruction_mutation", "projectionSemantics": "committed_instruction_fact_non_authoritative_state", @@ -73,7 +183,7 @@ impl crate::MtApiEventMaterializer for crate::MtMetadataMaterializer { "externalContentFetched": false, "externalAccountRead": false, "provenance": { - "processorName": crate::MT_METADATA_SPM_PROCESSOR_NAME, + "processorName": crate::MT_METADATA_SOLANA_PROGRAM_METADATA_PROCESSOR_NAME, "processorVersion": env!("CARGO_PKG_VERSION"), "decoderProof": observation.proof.clone() } @@ -141,18 +251,18 @@ fn instruction_projection(operation: &str, payload: &serde_json::Value) -> serde } fn state_transition(operation: &str) -> &'static str { - match operation { - "write" => return "buffer_content_written", - "initialize" => return "metadata_account_initialized", - "set_authority" => return "account_authority_updated", - "set_data" => return "metadata_data_configuration_updated", - "set_immutable" => return "metadata_account_marked_immutable", - "trim" => return "account_capacity_trimmed", - "close" => return "account_closed", - "allocate" => return "buffer_account_allocated", - "extend" => return "account_capacity_extended", - _ => return "unknown", - } + return match operation { + "write" => "buffer_content_written", + "initialize" => "metadata_account_initialized", + "set_authority" => "account_authority_updated", + "set_data" => "metadata_data_configuration_updated", + "set_immutable" => "metadata_account_marked_immutable", + "trim" => "account_capacity_trimmed", + "close" => "account_closed", + "allocate" => "buffer_account_allocated", + "extend" => "account_capacity_extended", + _ => "unknown", + }; } fn target_account(operation: &str, accounts: &serde_json::Value) -> serde_json::Value { @@ -194,10 +304,6 @@ fn account_for_role(accounts: &serde_json::Value, role: &str) -> serde_json::Val #[cfg(test)] mod tests { - const MATRIX_JSON: &str = include_str!( - "../../../../../test-fixtures/contract-matrices/SOLANA_PROGRAM_METADATA_MATERIALIZATION_MATRIX.json" - ); - fn observation(operation: &str, committed: bool) -> crate::DcApiDecodedObservation { return crate::DcApiDecodedObservation { event_key: format!("solana_program_metadata:{operation}:0"), @@ -249,7 +355,9 @@ mod tests { #[test] fn every_stable_instruction_has_one_materialized_mutation_fact() { - let materializer = crate::MtMetadataMaterializer; + let materializer = crate::MtSolanaProgramMetadataMaterializer; + let identity = crate::MtApiEventMaterializer::identity(&materializer); + assert_eq!(identity.name, crate::MT_METADATA_SOLANA_PROGRAM_METADATA_PROCESSOR_NAME); for (_, operation) in crate::DC_METADATA_SPM_INSTRUCTION_ENTRIES.iter().copied() { let value = observation(operation, true); assert!(crate::MtApiEventMaterializer::accepts_observation(&materializer, &value)); @@ -259,12 +367,16 @@ mod tests { assert_eq!(result.outputs[0].payload_json["operation"], *operation); assert_eq!(result.outputs[0].payload_json["projection"]["deprecated"], false); assert_eq!(result.outputs[0].payload_json["projection"]["replaced"], false); + assert_eq!( + result.outputs[0].payload_json["provenance"]["processorName"], + crate::MT_METADATA_SOLANA_PROGRAM_METADATA_PROCESSOR_NAME + ); } } #[test] fn failed_instruction_is_refused_and_foreign_surface_is_ignored() { - let materializer = crate::MtMetadataMaterializer; + let materializer = crate::MtSolanaProgramMetadataMaterializer; let failed = observation("set_data", false); let failure = crate::MtApiEventMaterializer::materialize(&materializer, &failed); assert_eq!(failure.status, crate::MtApiMaterializerOutcomeStatus::Refused); @@ -277,7 +389,9 @@ mod tests { #[test] fn materialization_matrix_matches_two_account_states_and_nine_instructions() { - let parsed = serde_json::from_str::(MATRIX_JSON); + let parsed = serde_json::from_str::(include_str!( + "../../../../../test-fixtures/contract-matrices/SOLANA_PROGRAM_METADATA_MATERIALIZATION_MATRIX.json" + )); assert!(parsed.is_ok()); let matrix = match parsed { std::result::Result::Ok(value) => value, @@ -310,7 +424,7 @@ mod tests { #[test] fn trim_keeps_prestable_alias_as_provenance_only() { - let materializer = crate::MtMetadataMaterializer; + let materializer = crate::MtSolanaProgramMetadataMaterializer; let value = observation("trim", true); let result = crate::MtApiEventMaterializer::materialize(&materializer, &value); assert_eq!( diff --git a/kb-lib/src/materializer/metadata/solana_program_metadata/state.rs b/kb-lib/src/materializer/metadata/solana_program_metadata/state.rs index 2d76eb7..7c0c92d 100644 --- a/kb-lib/src/materializer/metadata/solana_program_metadata/state.rs +++ b/kb-lib/src/materializer/metadata/solana_program_metadata/state.rs @@ -1,5 +1,5 @@ // file: kb-lib/src/materializer/metadata/solana_program_metadata/state.rs -// version: 2 +// version: 4 //! Authoritative Solana Program Metadata account-state projections. @@ -11,20 +11,18 @@ pub fn materializer_metadata_materialize_solana_program_metadata_account_snapsho slot: u64, snapshot: &crate::DcMetadataSpmAccountSnapshot, ) -> std::result::Result { - match snapshot { + return match snapshot { crate::DcMetadataSpmAccountSnapshot::Buffer(value) => { - return crate::materializer_metadata_materialize_solana_program_metadata_buffer_snapshot( - slot, - value, - ); + crate::materializer_metadata_materialize_solana_program_metadata_buffer_snapshot( + slot, value, + ) }, crate::DcMetadataSpmAccountSnapshot::Metadata(value) => { - return crate::materializer_metadata_materialize_solana_program_metadata_metadata_snapshot( - slot, - value, - ); + crate::materializer_metadata_materialize_solana_program_metadata_metadata_snapshot( + slot, value, + ) }, - } + }; } /// Materializes one bounded Solana Program Metadata Buffer account snapshot. @@ -65,7 +63,7 @@ pub fn materializer_metadata_materialize_solana_program_metadata_buffer_snapshot output_key: format!("solana_program_metadata_account_state:{}", snapshot.account), family: crate::MdMaterializedEventFamily::Metadata, payload_json: serde_json::json!({ - "projectionVersion": crate::MT_METADATA_SPM_PROJECTION_VERSION, + "projectionVersion": crate::MT_METADATA_SOLANA_PROGRAM_METADATA_PROJECTION_VERSION, "domain": "solana_program_metadata", "projectionKind": "buffer_state", "projectionSemantics": "authoritative_bounded_owned_account_snapshot", @@ -85,7 +83,7 @@ pub fn materializer_metadata_materialize_solana_program_metadata_buffer_snapshot "externalAccountRead": false, "instructionProjectionRole": "mutation_observation_only_no_duplicate_state", "provenance": { - "processorName": crate::MT_METADATA_SPM_PROCESSOR_NAME, + "processorName": crate::MT_METADATA_SOLANA_PROGRAM_METADATA_PROCESSOR_NAME, "processorVersion": env!("CARGO_PKG_VERSION"), "source": "bounded_solana_program_metadata_account_decoder" } @@ -152,7 +150,7 @@ pub fn materializer_metadata_materialize_solana_program_metadata_metadata_snapsh output_key: format!("solana_program_metadata_account_state:{}", snapshot.account), family: crate::MdMaterializedEventFamily::Metadata, payload_json: serde_json::json!({ - "projectionVersion": crate::MT_METADATA_SPM_PROJECTION_VERSION, + "projectionVersion": crate::MT_METADATA_SOLANA_PROGRAM_METADATA_PROJECTION_VERSION, "domain": "solana_program_metadata", "projectionKind": "metadata_state", "projectionSemantics": "authoritative_bounded_owned_account_snapshot", @@ -179,7 +177,7 @@ pub fn materializer_metadata_materialize_solana_program_metadata_metadata_snapsh "externalAccountRead": false, "instructionProjectionRole": "mutation_observation_only_no_duplicate_state", "provenance": { - "processorName": crate::MT_METADATA_SPM_PROCESSOR_NAME, + "processorName": crate::MT_METADATA_SOLANA_PROGRAM_METADATA_PROCESSOR_NAME, "processorVersion": env!("CARGO_PKG_VERSION"), "source": "bounded_solana_program_metadata_account_decoder" } @@ -188,32 +186,32 @@ pub fn materializer_metadata_materialize_solana_program_metadata_metadata_snapsh } fn data_projection(data: &crate::DcMetadataSpmData) -> serde_json::Value { - match data { + return match data { crate::DcMetadataSpmData::Direct(value) => { let mut projection = bytes_projection(value.as_slice()); projection["kind"] = serde_json::json!("direct"); - return projection; + projection }, crate::DcMetadataSpmData::Url(value) => { - return serde_json::json!({ + serde_json::json!({ "kind": "url", "value": value, "lengthBytes": value.len(), "sha256": hash(value.as_bytes()), "fetched": false - }); + }) }, crate::DcMetadataSpmData::External(value) => { - return serde_json::json!({ + serde_json::json!({ "kind": "external", "address": value.address.to_string(), "offset": value.offset, "length": value.length.map(std::num::NonZeroU32::get), "wireLengthBytes": crate::DC_METADATA_SPM_EXTERNAL_DATA_BYTES, "accountRead": false - }); + }) }, - } + }; } fn bytes_projection(bytes: &[u8]) -> serde_json::Value { diff --git a/kb-lib/src/materializer/metadata/token_2022.rs b/kb-lib/src/materializer/metadata/token_2022.rs new file mode 100644 index 0000000..a315741 --- /dev/null +++ b/kb-lib/src/materializer/metadata/token_2022.rs @@ -0,0 +1,19 @@ +// file: kb-lib/src/materializer/metadata/token_2022.rs +// version: 2 + +//! Embedded SPL Token-2022 metadata materialization surface. + +mod constants; +mod state; + +/// Materializes Token-2022 embedded metadata and group snapshots. +pub use self::state::materializer_metadata_materialize_token_2022_snapshot; + +/// Stable runtime component name for Token-2022 embedded metadata projections. +pub(crate) use self::constants::MT_METADATA_TOKEN_2022_COMPONENT_NAME; +/// Stable processor name for Token-2022 embedded metadata projections. +pub(crate) use self::constants::MT_METADATA_TOKEN_2022_PROCESSOR_NAME; +/// Stable Token-2022 embedded metadata projection version. +pub(crate) use self::constants::MT_METADATA_TOKEN_2022_PROJECTION_VERSION; +/// Canonical tracing target for Token-2022 embedded metadata materialization. +pub(crate) use self::constants::TRACING_TARGET_MATERIALIZER_METADATA_TOKEN_2022; diff --git a/kb-lib/src/materializer/metadata/token_2022/constants.rs b/kb-lib/src/materializer/metadata/token_2022/constants.rs new file mode 100644 index 0000000..f637b61 --- /dev/null +++ b/kb-lib/src/materializer/metadata/token_2022/constants.rs @@ -0,0 +1,15 @@ +// file: kb-lib/src/materializer/metadata/token_2022/constants.rs +// version: 3 + +//! Local constants for the metadata token_2022 materializer. + +/// Stable runtime component name for the metadata token_2022 materializer. +pub(crate) const MT_METADATA_TOKEN_2022_COMPONENT_NAME: &str = + "kb-lib.materializer.metadata.token_2022"; +/// Stable processor name persisted by metadata projections. +pub(crate) const MT_METADATA_TOKEN_2022_PROCESSOR_NAME: &str = "materializer.metadata.token_2022"; +/// Stable projection contract version. +pub(crate) const MT_METADATA_TOKEN_2022_PROJECTION_VERSION: u32 = 2; +/// Canonical tracing target for the metadata token_2022 materializer. +pub(crate) const TRACING_TARGET_MATERIALIZER_METADATA_TOKEN_2022: &str = + "kb-lib.materializer.metadata.token_2022"; diff --git a/kb-lib/src/materializer/metadata/token_2022/state.rs b/kb-lib/src/materializer/metadata/token_2022/state.rs new file mode 100644 index 0000000..34c7b3a --- /dev/null +++ b/kb-lib/src/materializer/metadata/token_2022/state.rs @@ -0,0 +1,210 @@ +// file: kb-lib/src/materializer/metadata/token_2022/state.rs +// version: 4 + +//! Authoritative embedded SPL Token-2022 metadata state materialization. + +/// Materialize embedded Token-2022 metadata, group, and group-member extensions. +pub fn materializer_metadata_materialize_token_2022_snapshot( + account_key: &str, + slot: u64, + state: &crate::DcToken2022State, +) -> std::result::Result, String> { + if account_key.is_empty() { + tracing::debug!( + target: crate::TRACING_TARGET_MATERIALIZER_METADATA_TOKEN_2022, + action = "materialize_token_2022_metadata_snapshot", + component_name = crate::MT_METADATA_TOKEN_2022_COMPONENT_NAME, + processor_name = crate::MT_METADATA_TOKEN_2022_PROCESSOR_NAME, + processor_version = env!("CARGO_PKG_VERSION"), + account_key = "", + slot, + status = "refused", + error_code = "empty_account_key", + "refuse Token-2022 metadata snapshot without account identity" + ); + return std::result::Result::Err( + "Token-2022 metadata snapshot requires a non-empty account key".to_string(), + ); + } + if state.kind != crate::DcToken2022StateKind::Mint { + tracing::debug!( + target: crate::TRACING_TARGET_MATERIALIZER_METADATA_TOKEN_2022, + action = "materialize_token_2022_metadata_snapshot", + component_name = crate::MT_METADATA_TOKEN_2022_COMPONENT_NAME, + processor_name = crate::MT_METADATA_TOKEN_2022_PROCESSOR_NAME, + processor_version = env!("CARGO_PKG_VERSION"), + account_key = %account_key, + slot, + state_kind = ?state.kind, + status = "refused", + error_code = "non_mint_state", + "refuse Token-2022 metadata snapshot for non-mint state" + ); + return std::result::Result::Err( + "Token-2022 metadata snapshot requires a parsed mint state".to_string(), + ); + } + let mut outputs = std::vec::Vec::new(); + for entry in &state.extensions { + let projection_kind = match entry.extension_name { + "token_metadata" => std::option::Option::Some("token_metadata"), + "token_group" => std::option::Option::Some("token_group"), + "token_group_member" => std::option::Option::Some("token_group_member"), + _ => std::option::Option::None, + }; + let kind = match projection_kind { + std::option::Option::Some(value) => value, + std::option::Option::None => continue, + }; + outputs.push(crate::MtApiMaterializedOutput { + output_key: format!("token_2022_metadata_snapshot:{account_key}:{slot}:{kind}"), + family: crate::MdMaterializedEventFamily::Metadata, + payload_json: serde_json::json!({ + "projectionVersion": crate::MT_METADATA_TOKEN_2022_PROJECTION_VERSION, + "domain": "spl_token_2022_embedded_metadata", + "projectionSemantics": "authoritative_bounded_mint_extension_snapshot", + "idempotenceKey": format!("token_2022_metadata:{account_key}:{slot}:{kind}"), + "accountKey": account_key, + "slot": slot, + "programId": kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID, + "metadataSource": "token_2022_embedded_tlv", + "projectionKind": kind, + "extensionType": entry.extension_type, + "extensionName": entry.extension_name, + "valueHex": entry.value_hex, + "valueFields": entry.value_fields, + "externalUriFetched": false, + "identityAuthority": "mint_address", + "finalAccountStateCaptured": true, + "provenance": { + "processorName": crate::MT_METADATA_TOKEN_2022_PROCESSOR_NAME, + "processorVersion": env!("CARGO_PKG_VERSION"), + "source": "bounded_token_2022_state_parser" + } + }), + }); + } + let status = if outputs.is_empty() { "ignored" } else { "inserted" }; + tracing::debug!( + target: crate::TRACING_TARGET_MATERIALIZER_METADATA_TOKEN_2022, + action = "materialize_token_2022_metadata_snapshot", + component_name = crate::MT_METADATA_TOKEN_2022_COMPONENT_NAME, + processor_name = crate::MT_METADATA_TOKEN_2022_PROCESSOR_NAME, + processor_version = env!("CARGO_PKG_VERSION"), + account_key = %account_key, + slot, + state_kind = "mint", + output_count = outputs.len(), + status, + "materialize embedded Token-2022 metadata state snapshots" + ); + return std::result::Result::Ok(outputs); +} + +#[cfg(test)] +mod tests_token_2022_state { + #[test] + fn embedded_metadata_group_and_member_have_distinct_stable_outputs() { + let state = crate::DcToken2022State { + kind: crate::DcToken2022StateKind::Mint, + base_fields: serde_json::json!({"supply":"1","decimals":0}), + base_hex: "00".repeat(82), + account_type: std::option::Option::Some(1), + extensions: std::vec![ + crate::DcToken2022TlvEntry { + extension_type: 19, + extension_name: "token_metadata", + value_hex: "01".to_string(), + value_fields: serde_json::json!({"name":"Token","symbol":"TOK","uri":"https://example.invalid/token.json"}), + }, + crate::DcToken2022TlvEntry { + extension_type: 20, + extension_name: "token_group", + value_hex: "02".to_string(), + value_fields: serde_json::json!({"mint":"mint111","size":"1","maxSize":"10"}), + }, + crate::DcToken2022TlvEntry { + extension_type: 21, + extension_name: "token_group_member", + value_hex: "03".to_string(), + value_fields: serde_json::json!({"mint":"mint111","group":"group111","memberNumber":"1"}), + }, + ], + }; + let outputs = + crate::materializer_metadata_materialize_token_2022_snapshot("mint111", 9, &state); + assert_eq!(outputs.as_ref().map(|values| return values.len()), std::result::Result::Ok(3)); + assert_eq!( + outputs + .as_ref() + .map(|values| return values[0].payload_json["metadataSource"].clone()), + std::result::Result::Ok(serde_json::json!("token_2022_embedded_tlv")) + ); + assert_eq!( + outputs + .as_ref() + .map(|values| return values[0].payload_json["externalUriFetched"].clone()), + std::result::Result::Ok(serde_json::json!(false)) + ); + assert_eq!( + outputs + .as_ref() + .map(|values| return values[1].payload_json["projectionKind"].clone()), + std::result::Result::Ok(serde_json::json!("token_group")) + ); + assert_eq!( + outputs.as_ref().map(|values| { + return values[0].payload_json["provenance"]["processorName"].clone(); + }), + std::result::Result::Ok(serde_json::json!( + crate::MT_METADATA_TOKEN_2022_PROCESSOR_NAME + )) + ); + assert_eq!( + outputs + .as_ref() + .map(|values| return values[0].payload_json["projectionVersion"].clone()), + std::result::Result::Ok(serde_json::json!(2)) + ); + } + + #[test] + fn snapshot_is_idempotent_filters_unrelated_extensions_and_requires_mint_identity() { + let state = crate::DcToken2022State { + kind: crate::DcToken2022StateKind::Mint, + base_fields: serde_json::json!({}), + base_hex: "00".repeat(82), + account_type: std::option::Option::Some(1), + extensions: std::vec![crate::DcToken2022TlvEntry { + extension_type: 1, + extension_name: "transfer_fee_config", + value_hex: "00".to_string(), + value_fields: serde_json::json!({}), + }], + }; + let first = + crate::materializer_metadata_materialize_token_2022_snapshot("mint111", 9, &state); + let second = + crate::materializer_metadata_materialize_token_2022_snapshot("mint111", 9, &state); + assert_eq!(first, second); + assert_eq!(first.as_ref().map(|values| return values.len()), std::result::Result::Ok(0)); + assert!( + crate::materializer_metadata_materialize_token_2022_snapshot("", 9, &state).is_err() + ); + let account_state = crate::DcToken2022State { + kind: crate::DcToken2022StateKind::Account, + base_fields: serde_json::json!({}), + base_hex: "00".repeat(165), + account_type: std::option::Option::None, + extensions: std::vec::Vec::new(), + }; + assert!( + crate::materializer_metadata_materialize_token_2022_snapshot( + "account111", + 9, + &account_state + ) + .is_err() + ); + } +} diff --git a/kb-lib/src/materializer/nft.rs b/kb-lib/src/materializer/nft.rs index b926ad6..526bc77 100644 --- a/kb-lib/src/materializer/nft.rs +++ b/kb-lib/src/materializer/nft.rs @@ -1,9 +1,17 @@ // file: kb-lib/src/materializer/nft.rs -// version: 3 +// version: 4 //! `nft` materializer family. -mod core; +mod constants; +mod materializer; /// Reserved NFT materializer. -pub use self::core::MtNftMaterializer; +pub use self::materializer::MtNftMaterializer; + +/// Empty accepted-family boundary for the reserved component. +pub(crate) use self::constants::MT_NFT_ACCEPTED_FAMILIES; +/// Stable runtime component name for the `nft` materializer. +pub(crate) use self::constants::MT_NFT_COMPONENT_NAME; +/// Stable processor name for the `nft` materializer. +pub(crate) use self::constants::MT_NFT_PROCESSOR_NAME; diff --git a/kb-lib/src/materializer/nft/constants.rs b/kb-lib/src/materializer/nft/constants.rs new file mode 100644 index 0000000..6f08bd3 --- /dev/null +++ b/kb-lib/src/materializer/nft/constants.rs @@ -0,0 +1,11 @@ +// file: kb-lib/src/materializer/nft/constants.rs +// version: 1 + +//! Local constants for the nft materializer. + +/// Stable runtime component name for the nft materializer. +pub(crate) const MT_NFT_COMPONENT_NAME: &str = "kb-lib.materializer.nft"; +/// Stable processor name persisted by the replay ledger. +pub(crate) const MT_NFT_PROCESSOR_NAME: &str = "materializer.nft"; +/// Empty family boundary retained while the materializer is reserved. +pub(crate) const MT_NFT_ACCEPTED_FAMILIES: &[crate::MdEventFamily] = &[]; diff --git a/kb-lib/src/materializer/nft/core.rs b/kb-lib/src/materializer/nft/materializer.rs similarity index 72% rename from kb-lib/src/materializer/nft/core.rs rename to kb-lib/src/materializer/nft/materializer.rs index 5b59dc1..4b311f4 100644 --- a/kb-lib/src/materializer/nft/core.rs +++ b/kb-lib/src/materializer/nft/materializer.rs @@ -1,5 +1,5 @@ -// file: kb-lib/src/materializer/nft/core.rs -// version: 4 +// file: kb-lib/src/materializer/nft/materializer.rs +// version: 5 //! Reserved NFT materializer. @@ -9,7 +9,7 @@ pub struct MtNftMaterializer; impl crate::MtMaterializer for crate::MtNftMaterializer { fn materializer_name(&self) -> &'static str { - return "kb-lib.materializer.nft"; + return crate::MT_NFT_COMPONENT_NAME; } fn materializer_version(&self) -> &'static str { @@ -22,27 +22,22 @@ impl crate::MtMaterializer for crate::MtNftMaterializer { fn materialize_event( &self, - event: &crate::MdDecodedProtocolEvent, + _event: &crate::MdDecodedProtocolEvent, ) -> kb_core::Result> { - return std::result::Result::Ok(std::vec![crate::MdMaterializedEvent { - signature: event.signature.clone(), - slot: event.slot, - decoded_family: event.event_family, - materialized_family: crate::MdMaterializedEventFamily::Nft, - }]); + return std::result::Result::Ok(std::vec::Vec::new()); } } impl crate::MtApiEventMaterializer for crate::MtNftMaterializer { fn identity(&self) -> crate::MtApiMaterializerIdentity { return crate::MtApiMaterializerIdentity { - name: "kb-lib.materializer.nft".to_string(), + name: crate::MT_NFT_PROCESSOR_NAME.to_string(), version: env!("CARGO_PKG_VERSION").to_string(), }; } fn accepted_families(&self) -> &'static [crate::MdEventFamily] { - return &[]; + return crate::MT_NFT_ACCEPTED_FAMILIES; } fn transaction_policy( diff --git a/kb-lib/src/materializer/oracle.rs b/kb-lib/src/materializer/oracle.rs index 9a58713..50e5a44 100644 --- a/kb-lib/src/materializer/oracle.rs +++ b/kb-lib/src/materializer/oracle.rs @@ -1,9 +1,17 @@ // file: kb-lib/src/materializer/oracle.rs -// version: 3 +// version: 4 //! `oracle` materializer family. -mod core; +mod constants; +mod materializer; /// Reserved oracle materializer. -pub use self::core::MtOracleMaterializer; +pub use self::materializer::MtOracleMaterializer; + +/// Empty accepted-family boundary for the reserved component. +pub(crate) use self::constants::MT_ORACLE_ACCEPTED_FAMILIES; +/// Stable runtime component name for the `oracle` materializer. +pub(crate) use self::constants::MT_ORACLE_COMPONENT_NAME; +/// Stable processor name for the `oracle` materializer. +pub(crate) use self::constants::MT_ORACLE_PROCESSOR_NAME; diff --git a/kb-lib/src/materializer/oracle/constants.rs b/kb-lib/src/materializer/oracle/constants.rs new file mode 100644 index 0000000..dd1b542 --- /dev/null +++ b/kb-lib/src/materializer/oracle/constants.rs @@ -0,0 +1,11 @@ +// file: kb-lib/src/materializer/oracle/constants.rs +// version: 1 + +//! Local constants for the oracle materializer. + +/// Stable runtime component name for the oracle materializer. +pub(crate) const MT_ORACLE_COMPONENT_NAME: &str = "kb-lib.materializer.oracle"; +/// Stable processor name persisted by the replay ledger. +pub(crate) const MT_ORACLE_PROCESSOR_NAME: &str = "materializer.oracle"; +/// Empty family boundary retained while the materializer is reserved. +pub(crate) const MT_ORACLE_ACCEPTED_FAMILIES: &[crate::MdEventFamily] = &[]; diff --git a/kb-lib/src/materializer/oracle/core.rs b/kb-lib/src/materializer/oracle/materializer.rs similarity index 72% rename from kb-lib/src/materializer/oracle/core.rs rename to kb-lib/src/materializer/oracle/materializer.rs index 6bb8adb..9070d49 100644 --- a/kb-lib/src/materializer/oracle/core.rs +++ b/kb-lib/src/materializer/oracle/materializer.rs @@ -1,5 +1,5 @@ -// file: kb-lib/src/materializer/oracle/core.rs -// version: 4 +// file: kb-lib/src/materializer/oracle/materializer.rs +// version: 5 //! Reserved oracle materializer. @@ -9,7 +9,7 @@ pub struct MtOracleMaterializer; impl crate::MtMaterializer for crate::MtOracleMaterializer { fn materializer_name(&self) -> &'static str { - return "kb-lib.materializer.oracle"; + return crate::MT_ORACLE_COMPONENT_NAME; } fn materializer_version(&self) -> &'static str { @@ -22,27 +22,22 @@ impl crate::MtMaterializer for crate::MtOracleMaterializer { fn materialize_event( &self, - event: &crate::MdDecodedProtocolEvent, + _event: &crate::MdDecodedProtocolEvent, ) -> kb_core::Result> { - return std::result::Result::Ok(std::vec![crate::MdMaterializedEvent { - signature: event.signature.clone(), - slot: event.slot, - decoded_family: event.event_family, - materialized_family: crate::MdMaterializedEventFamily::Oracle, - }]); + return std::result::Result::Ok(std::vec::Vec::new()); } } impl crate::MtApiEventMaterializer for crate::MtOracleMaterializer { fn identity(&self) -> crate::MtApiMaterializerIdentity { return crate::MtApiMaterializerIdentity { - name: "kb-lib.materializer.oracle".to_string(), + name: crate::MT_ORACLE_PROCESSOR_NAME.to_string(), version: env!("CARGO_PKG_VERSION").to_string(), }; } fn accepted_families(&self) -> &'static [crate::MdEventFamily] { - return &[]; + return crate::MT_ORACLE_ACCEPTED_FAMILIES; } fn transaction_policy( diff --git a/kb-lib/src/materializer/orderbook.rs b/kb-lib/src/materializer/orderbook.rs index 633d315..07a8a9a 100644 --- a/kb-lib/src/materializer/orderbook.rs +++ b/kb-lib/src/materializer/orderbook.rs @@ -1,9 +1,17 @@ // file: kb-lib/src/materializer/orderbook.rs -// version: 3 +// version: 4 //! `orderbook` materializer family. -mod core; +mod constants; +mod materializer; /// Reserved orderbook materializer. -pub use self::core::MtOrderbookMaterializer; +pub use self::materializer::MtOrderbookMaterializer; + +/// Empty accepted-family boundary for the reserved component. +pub(crate) use self::constants::MT_ORDERBOOK_ACCEPTED_FAMILIES; +/// Stable runtime component name for the `orderbook` materializer. +pub(crate) use self::constants::MT_ORDERBOOK_COMPONENT_NAME; +/// Stable processor name for the `orderbook` materializer. +pub(crate) use self::constants::MT_ORDERBOOK_PROCESSOR_NAME; diff --git a/kb-lib/src/materializer/orderbook/constants.rs b/kb-lib/src/materializer/orderbook/constants.rs new file mode 100644 index 0000000..d2157bd --- /dev/null +++ b/kb-lib/src/materializer/orderbook/constants.rs @@ -0,0 +1,11 @@ +// file: kb-lib/src/materializer/orderbook/constants.rs +// version: 1 + +//! Local constants for the orderbook materializer. + +/// Stable runtime component name for the orderbook materializer. +pub(crate) const MT_ORDERBOOK_COMPONENT_NAME: &str = "kb-lib.materializer.orderbook"; +/// Stable processor name persisted by the replay ledger. +pub(crate) const MT_ORDERBOOK_PROCESSOR_NAME: &str = "materializer.orderbook"; +/// Empty family boundary retained while the materializer is reserved. +pub(crate) const MT_ORDERBOOK_ACCEPTED_FAMILIES: &[crate::MdEventFamily] = &[]; diff --git a/kb-lib/src/materializer/orderbook/core.rs b/kb-lib/src/materializer/orderbook/materializer.rs similarity index 72% rename from kb-lib/src/materializer/orderbook/core.rs rename to kb-lib/src/materializer/orderbook/materializer.rs index 00cc6fb..f0d1036 100644 --- a/kb-lib/src/materializer/orderbook/core.rs +++ b/kb-lib/src/materializer/orderbook/materializer.rs @@ -1,5 +1,5 @@ -// file: kb-lib/src/materializer/orderbook/core.rs -// version: 4 +// file: kb-lib/src/materializer/orderbook/materializer.rs +// version: 5 //! Reserved orderbook materializer. @@ -9,7 +9,7 @@ pub struct MtOrderbookMaterializer; impl crate::MtMaterializer for crate::MtOrderbookMaterializer { fn materializer_name(&self) -> &'static str { - return "kb-lib.materializer.orderbook"; + return crate::MT_ORDERBOOK_COMPONENT_NAME; } fn materializer_version(&self) -> &'static str { @@ -22,27 +22,22 @@ impl crate::MtMaterializer for crate::MtOrderbookMaterializer { fn materialize_event( &self, - event: &crate::MdDecodedProtocolEvent, + _event: &crate::MdDecodedProtocolEvent, ) -> kb_core::Result> { - return std::result::Result::Ok(std::vec![crate::MdMaterializedEvent { - signature: event.signature.clone(), - slot: event.slot, - decoded_family: event.event_family, - materialized_family: crate::MdMaterializedEventFamily::Orderbook, - }]); + return std::result::Result::Ok(std::vec::Vec::new()); } } impl crate::MtApiEventMaterializer for crate::MtOrderbookMaterializer { fn identity(&self) -> crate::MtApiMaterializerIdentity { return crate::MtApiMaterializerIdentity { - name: "kb-lib.materializer.orderbook".to_string(), + name: crate::MT_ORDERBOOK_PROCESSOR_NAME.to_string(), version: env!("CARGO_PKG_VERSION").to_string(), }; } fn accepted_families(&self) -> &'static [crate::MdEventFamily] { - return &[]; + return crate::MT_ORDERBOOK_ACCEPTED_FAMILIES; } fn transaction_policy( diff --git a/kb-lib/src/materializer/perpetuals.rs b/kb-lib/src/materializer/perpetuals.rs index 7adbbe6..d129c1d 100644 --- a/kb-lib/src/materializer/perpetuals.rs +++ b/kb-lib/src/materializer/perpetuals.rs @@ -1,9 +1,17 @@ // file: kb-lib/src/materializer/perpetuals.rs -// version: 3 +// version: 4 //! `perpetuals` materializer family. -mod core; +mod constants; +mod materializer; /// Reserved perpetuals materializer. -pub use self::core::MtPerpetualsMaterializer; +pub use self::materializer::MtPerpetualsMaterializer; + +/// Empty accepted-family boundary for the reserved component. +pub(crate) use self::constants::MT_PERPETUALS_ACCEPTED_FAMILIES; +/// Stable runtime component name for the `perpetuals` materializer. +pub(crate) use self::constants::MT_PERPETUALS_COMPONENT_NAME; +/// Stable processor name for the `perpetuals` materializer. +pub(crate) use self::constants::MT_PERPETUALS_PROCESSOR_NAME; diff --git a/kb-lib/src/materializer/perpetuals/constants.rs b/kb-lib/src/materializer/perpetuals/constants.rs new file mode 100644 index 0000000..6878af8 --- /dev/null +++ b/kb-lib/src/materializer/perpetuals/constants.rs @@ -0,0 +1,11 @@ +// file: kb-lib/src/materializer/perpetuals/constants.rs +// version: 1 + +//! Local constants for the perpetuals materializer. + +/// Stable runtime component name for the perpetuals materializer. +pub(crate) const MT_PERPETUALS_COMPONENT_NAME: &str = "kb-lib.materializer.perpetuals"; +/// Stable processor name persisted by the replay ledger. +pub(crate) const MT_PERPETUALS_PROCESSOR_NAME: &str = "materializer.perpetuals"; +/// Empty family boundary retained while the materializer is reserved. +pub(crate) const MT_PERPETUALS_ACCEPTED_FAMILIES: &[crate::MdEventFamily] = &[]; diff --git a/kb-lib/src/materializer/perpetuals/core.rs b/kb-lib/src/materializer/perpetuals/materializer.rs similarity index 72% rename from kb-lib/src/materializer/perpetuals/core.rs rename to kb-lib/src/materializer/perpetuals/materializer.rs index e57bce2..6ce0869 100644 --- a/kb-lib/src/materializer/perpetuals/core.rs +++ b/kb-lib/src/materializer/perpetuals/materializer.rs @@ -1,5 +1,5 @@ -// file: kb-lib/src/materializer/perpetuals/core.rs -// version: 3 +// file: kb-lib/src/materializer/perpetuals/materializer.rs +// version: 4 //! Reserved perpetuals materializer. @@ -9,7 +9,7 @@ pub struct MtPerpetualsMaterializer; impl crate::MtMaterializer for crate::MtPerpetualsMaterializer { fn materializer_name(&self) -> &'static str { - return "kb-lib.materializer.perpetuals"; + return crate::MT_PERPETUALS_COMPONENT_NAME; } fn materializer_version(&self) -> &'static str { @@ -22,27 +22,22 @@ impl crate::MtMaterializer for crate::MtPerpetualsMaterializer { fn materialize_event( &self, - event: &crate::MdDecodedProtocolEvent, + _event: &crate::MdDecodedProtocolEvent, ) -> kb_core::Result> { - return std::result::Result::Ok(std::vec![crate::MdMaterializedEvent { - signature: event.signature.clone(), - slot: event.slot, - decoded_family: event.event_family, - materialized_family: crate::MdMaterializedEventFamily::Perpetuals, - }]); + return std::result::Result::Ok(std::vec::Vec::new()); } } impl crate::MtApiEventMaterializer for crate::MtPerpetualsMaterializer { fn identity(&self) -> crate::MtApiMaterializerIdentity { return crate::MtApiMaterializerIdentity { - name: "kb-lib.materializer.perpetuals".to_string(), + name: crate::MT_PERPETUALS_PROCESSOR_NAME.to_string(), version: env!("CARGO_PKG_VERSION").to_string(), }; } fn accepted_families(&self) -> &'static [crate::MdEventFamily] { - return &[]; + return crate::MT_PERPETUALS_ACCEPTED_FAMILIES; } fn transaction_policy( diff --git a/kb-lib/src/materializer/pool.rs b/kb-lib/src/materializer/pool.rs index a9623e3..232ff2e 100644 --- a/kb-lib/src/materializer/pool.rs +++ b/kb-lib/src/materializer/pool.rs @@ -1,5 +1,5 @@ // file: kb-lib/src/materializer/pool.rs -// version: 3 +// version: 4 //! `pool` materializer family. @@ -7,3 +7,10 @@ mod state; /// Reserved pool-state materializer. pub use self::state::MtPoolStateMaterializer; + +/// Empty accepted-family boundary for the reserved component. +pub(crate) use self::state::MT_POOL_STATE_ACCEPTED_FAMILIES; +/// Stable runtime component name for the reserved pool-state materializer. +pub(crate) use self::state::MT_POOL_STATE_COMPONENT_NAME; +/// Stable processor name for the reserved pool-state materializer. +pub(crate) use self::state::MT_POOL_STATE_PROCESSOR_NAME; diff --git a/kb-lib/src/materializer/pool/state.rs b/kb-lib/src/materializer/pool/state.rs index 7197369..17a1c69 100644 --- a/kb-lib/src/materializer/pool/state.rs +++ b/kb-lib/src/materializer/pool/state.rs @@ -1,61 +1,17 @@ // file: kb-lib/src/materializer/pool/state.rs -// version: 4 +// version: 5 -//! Reserved pool-state materializer. +//! Pool-state materializer component facade. -/// Reserved materializer for pool-state observations. -#[derive(Clone, Debug, Default)] -pub struct MtPoolStateMaterializer; +mod constants; +mod materializer; -impl crate::MtMaterializer for crate::MtPoolStateMaterializer { - fn materializer_name(&self) -> &'static str { - return "kb-lib.materializer.pool.state"; - } +/// Reserved pool-state materializer. +pub use self::materializer::MtPoolStateMaterializer; - fn materializer_version(&self) -> &'static str { - return env!("CARGO_PKG_VERSION"); - } - - fn accepts_event(&self, _event: &crate::MdDecodedProtocolEvent) -> bool { - return false; - } - - fn materialize_event( - &self, - event: &crate::MdDecodedProtocolEvent, - ) -> kb_core::Result> { - return std::result::Result::Ok(std::vec![crate::MdMaterializedEvent { - signature: event.signature.clone(), - slot: event.slot, - decoded_family: event.event_family, - materialized_family: crate::MdMaterializedEventFamily::PoolState, - }]); - } -} - -impl crate::MtApiEventMaterializer for crate::MtPoolStateMaterializer { - fn identity(&self) -> crate::MtApiMaterializerIdentity { - return crate::MtApiMaterializerIdentity { - name: "kb-lib.materializer.pool.state".to_string(), - version: env!("CARGO_PKG_VERSION").to_string(), - }; - } - - fn accepted_families(&self) -> &'static [crate::MdEventFamily] { - return &[]; - } - - fn transaction_policy( - &self, - _family: crate::MdEventFamily, - ) -> crate::MtApiMaterializationTransactionPolicy { - return crate::MtApiMaterializationTransactionPolicy::SuccessfulCommittedOnly; - } - - fn materialize( - &self, - _observation: &crate::DcApiDecodedObservation, - ) -> crate::MtApiMaterializerExecutionResult { - return crate::MtApiMaterializerExecutionResult::ignored(); - } -} +/// Empty accepted-family boundary for the reserved component. +pub(crate) use self::constants::MT_POOL_STATE_ACCEPTED_FAMILIES; +/// Stable runtime component name for the reserved pool-state materializer. +pub(crate) use self::constants::MT_POOL_STATE_COMPONENT_NAME; +/// Stable processor name for the reserved pool-state materializer. +pub(crate) use self::constants::MT_POOL_STATE_PROCESSOR_NAME; diff --git a/kb-lib/src/materializer/pool/state/constants.rs b/kb-lib/src/materializer/pool/state/constants.rs new file mode 100644 index 0000000..e566166 --- /dev/null +++ b/kb-lib/src/materializer/pool/state/constants.rs @@ -0,0 +1,11 @@ +// file: kb-lib/src/materializer/pool/state/constants.rs +// version: 1 + +//! Local constants for the pool state materializer. + +/// Stable runtime component name for the pool state materializer. +pub(crate) const MT_POOL_STATE_COMPONENT_NAME: &str = "kb-lib.materializer.pool.state"; +/// Stable processor name persisted by the replay ledger. +pub(crate) const MT_POOL_STATE_PROCESSOR_NAME: &str = "materializer.pool.state"; +/// Empty family boundary retained while the materializer is reserved. +pub(crate) const MT_POOL_STATE_ACCEPTED_FAMILIES: &[crate::MdEventFamily] = &[]; diff --git a/kb-lib/src/materializer/pool/state/materializer.rs b/kb-lib/src/materializer/pool/state/materializer.rs new file mode 100644 index 0000000..b8f06d0 --- /dev/null +++ b/kb-lib/src/materializer/pool/state/materializer.rs @@ -0,0 +1,56 @@ +// file: kb-lib/src/materializer/pool/state/materializer.rs +// version: 5 + +//! Reserved pool-state materializer. + +/// Reserved materializer for pool-state observations. +#[derive(Clone, Debug, Default)] +pub struct MtPoolStateMaterializer; + +impl crate::MtMaterializer for crate::MtPoolStateMaterializer { + fn materializer_name(&self) -> &'static str { + return crate::MT_POOL_STATE_COMPONENT_NAME; + } + + fn materializer_version(&self) -> &'static str { + return env!("CARGO_PKG_VERSION"); + } + + fn accepts_event(&self, _event: &crate::MdDecodedProtocolEvent) -> bool { + return false; + } + + fn materialize_event( + &self, + _event: &crate::MdDecodedProtocolEvent, + ) -> kb_core::Result> { + return std::result::Result::Ok(std::vec::Vec::new()); + } +} + +impl crate::MtApiEventMaterializer for crate::MtPoolStateMaterializer { + fn identity(&self) -> crate::MtApiMaterializerIdentity { + return crate::MtApiMaterializerIdentity { + name: crate::MT_POOL_STATE_PROCESSOR_NAME.to_string(), + version: env!("CARGO_PKG_VERSION").to_string(), + }; + } + + fn accepted_families(&self) -> &'static [crate::MdEventFamily] { + return crate::MT_POOL_STATE_ACCEPTED_FAMILIES; + } + + fn transaction_policy( + &self, + _family: crate::MdEventFamily, + ) -> crate::MtApiMaterializationTransactionPolicy { + return crate::MtApiMaterializationTransactionPolicy::SuccessfulCommittedOnly; + } + + fn materialize( + &self, + _observation: &crate::DcApiDecodedObservation, + ) -> crate::MtApiMaterializerExecutionResult { + return crate::MtApiMaterializerExecutionResult::ignored(); + } +} diff --git a/kb-lib/src/materializer/rewards.rs b/kb-lib/src/materializer/rewards.rs index 99e0a9c..69fb7dc 100644 --- a/kb-lib/src/materializer/rewards.rs +++ b/kb-lib/src/materializer/rewards.rs @@ -1,9 +1,17 @@ // file: kb-lib/src/materializer/rewards.rs -// version: 3 +// version: 4 //! `rewards` materializer family. -mod core; +mod constants; +mod materializer; /// Reserved rewards materializer. -pub use self::core::MtRewardsMaterializer; +pub use self::materializer::MtRewardsMaterializer; + +/// Empty accepted-family boundary for the reserved component. +pub(crate) use self::constants::MT_REWARDS_ACCEPTED_FAMILIES; +/// Stable runtime component name for the `rewards` materializer. +pub(crate) use self::constants::MT_REWARDS_COMPONENT_NAME; +/// Stable processor name for the `rewards` materializer. +pub(crate) use self::constants::MT_REWARDS_PROCESSOR_NAME; diff --git a/kb-lib/src/materializer/rewards/constants.rs b/kb-lib/src/materializer/rewards/constants.rs new file mode 100644 index 0000000..07a6314 --- /dev/null +++ b/kb-lib/src/materializer/rewards/constants.rs @@ -0,0 +1,11 @@ +// file: kb-lib/src/materializer/rewards/constants.rs +// version: 1 + +//! Local constants for the rewards materializer. + +/// Stable runtime component name for the rewards materializer. +pub(crate) const MT_REWARDS_COMPONENT_NAME: &str = "kb-lib.materializer.rewards"; +/// Stable processor name persisted by the replay ledger. +pub(crate) const MT_REWARDS_PROCESSOR_NAME: &str = "materializer.rewards"; +/// Empty family boundary retained while the materializer is reserved. +pub(crate) const MT_REWARDS_ACCEPTED_FAMILIES: &[crate::MdEventFamily] = &[]; diff --git a/kb-lib/src/materializer/rewards/core.rs b/kb-lib/src/materializer/rewards/materializer.rs similarity index 69% rename from kb-lib/src/materializer/rewards/core.rs rename to kb-lib/src/materializer/rewards/materializer.rs index b3f62c5..fe8eb1a 100644 --- a/kb-lib/src/materializer/rewards/core.rs +++ b/kb-lib/src/materializer/rewards/materializer.rs @@ -1,15 +1,15 @@ -// file: kb-lib/src/materializer/rewards/core.rs -// version: 4 +// file: kb-lib/src/materializer/rewards/materializer.rs +// version: 5 //! Reserved rewards materializer. -/// Reserved materializer for reward observations. +/// Reserved materializer for rewards observations. #[derive(Clone, Debug, Default)] pub struct MtRewardsMaterializer; impl crate::MtMaterializer for crate::MtRewardsMaterializer { fn materializer_name(&self) -> &'static str { - return "kb-lib.materializer.rewards"; + return crate::MT_REWARDS_COMPONENT_NAME; } fn materializer_version(&self) -> &'static str { @@ -22,27 +22,22 @@ impl crate::MtMaterializer for crate::MtRewardsMaterializer { fn materialize_event( &self, - event: &crate::MdDecodedProtocolEvent, + _event: &crate::MdDecodedProtocolEvent, ) -> kb_core::Result> { - return std::result::Result::Ok(std::vec![crate::MdMaterializedEvent { - signature: event.signature.clone(), - slot: event.slot, - decoded_family: event.event_family, - materialized_family: crate::MdMaterializedEventFamily::Reward, - }]); + return std::result::Result::Ok(std::vec::Vec::new()); } } impl crate::MtApiEventMaterializer for crate::MtRewardsMaterializer { fn identity(&self) -> crate::MtApiMaterializerIdentity { return crate::MtApiMaterializerIdentity { - name: "kb-lib.materializer.rewards".to_string(), + name: crate::MT_REWARDS_PROCESSOR_NAME.to_string(), version: env!("CARGO_PKG_VERSION").to_string(), }; } fn accepted_families(&self) -> &'static [crate::MdEventFamily] { - return &[]; + return crate::MT_REWARDS_ACCEPTED_FAMILIES; } fn transaction_policy( diff --git a/kb-lib/src/materializer/risk.rs b/kb-lib/src/materializer/risk.rs index f96a07c..8baf6d6 100644 --- a/kb-lib/src/materializer/risk.rs +++ b/kb-lib/src/materializer/risk.rs @@ -1,17 +1,23 @@ // file: kb-lib/src/materializer/risk.rs -// version: 3 +// version: 4 //! `risk` materializer family. mod constants; -mod core; +mod materializer; /// Stable SPL Token and ATA risk materializer. -pub use self::core::MtRiskMaterializer; +pub use self::materializer::MtRiskMaterializer; -/// Stable processor name for SPL Token and ATA risk facts. +/// Crate-visible `MT_RISK_ACCEPTED_FAMILIES` component constant. +pub(crate) use self::constants::MT_RISK_ACCEPTED_FAMILIES; +/// Stable runtime component name for the `risk` materializer. +pub(crate) use self::constants::MT_RISK_COMPONENT_NAME; +/// Stable processor name for the `risk` materializer. pub(crate) use self::constants::MT_RISK_PROCESSOR_NAME; -/// Stable projection contract version for SPL Token and ATA risk facts. +/// Stable projection version for the `risk` materializer. pub(crate) use self::constants::MT_RISK_PROJECTION_VERSION; -/// Canonical tracing target for SPL Token and ATA risk facts. +/// Crate-visible `MT_RISK_RISK_ENTRIES` component constant. +pub(crate) use self::constants::MT_RISK_RISK_ENTRIES; +/// Canonical tracing target for the `risk` materializer. pub(crate) use self::constants::TRACING_TARGET_MATERIALIZER_RISK; diff --git a/kb-lib/src/materializer/risk/constants.rs b/kb-lib/src/materializer/risk/constants.rs index b825fbf..9ba590f 100644 --- a/kb-lib/src/materializer/risk/constants.rs +++ b/kb-lib/src/materializer/risk/constants.rs @@ -1,11 +1,48 @@ // file: kb-lib/src/materializer/risk/constants.rs -// version: 3 +// version: 5 -//! Local constants for fact-only risk projections. +//! Local constants for the risk materializer. +/// Stable runtime component name for the risk materializer. +pub(crate) const MT_RISK_COMPONENT_NAME: &str = "kb-lib.materializer.risk"; /// Stable processor name persisted by the replay ledger. pub(crate) const MT_RISK_PROCESSOR_NAME: &str = "materializer.risk"; -/// Stable projection payload contract version. +/// Stable projection contract version for the risk materializer. pub(crate) const MT_RISK_PROJECTION_VERSION: u32 = 2; -/// Canonical tracing target for this component. +/// Canonical tracing target for the risk materializer. pub(crate) const TRACING_TARGET_MATERIALIZER_RISK: &str = "kb-lib.materializer.risk"; + +/// Component contract for `accepted_families`. +pub(crate) const MT_RISK_ACCEPTED_FAMILIES: &[crate::MdEventFamily] = &[ + crate::MdEventFamily::TokenAccount, + crate::MdEventFamily::TokenMint, + crate::MdEventFamily::Admin, + crate::MdEventFamily::Lifecycle, + crate::MdEventFamily::Audit, +]; +/// Component contract for `risk_entries`. +pub(crate) const MT_RISK_RISK_ENTRIES: &[&str] = &[ + "approve", + "approve_checked", + "revoke", + "freeze_account", + "thaw_account", + "set_authority", + "initialize_multisig", + "initialize_multisig2", + "initialize_default_account_state", + "update_default_account_state", + "enable_required_transfer_memos", + "disable_required_transfer_memos", + "enable_cpi_guard", + "disable_cpi_guard", + "initialize_non_transferable_mint", + "initialize_pausable_config", + "pause", + "resume", + "initialize_permissioned_burn", + "enable_confidential_credits", + "disable_confidential_credits", + "enable_non_confidential_credits", + "disable_non_confidential_credits", +]; diff --git a/kb-lib/src/materializer/risk/core.rs b/kb-lib/src/materializer/risk/materializer.rs similarity index 95% rename from kb-lib/src/materializer/risk/core.rs rename to kb-lib/src/materializer/risk/materializer.rs index 54e863e..166850c 100644 --- a/kb-lib/src/materializer/risk/core.rs +++ b/kb-lib/src/materializer/risk/materializer.rs @@ -1,48 +1,15 @@ -// file: kb-lib/src/materializer/risk/core.rs -// version: 6 +// file: kb-lib/src/materializer/risk/materializer.rs +// version: 8 //! Stable fact-only risk projections for committed SPL Token and ATA observations. -const ACCEPTED_FAMILIES: &[crate::MdEventFamily] = &[ - crate::MdEventFamily::TokenAccount, - crate::MdEventFamily::TokenMint, - crate::MdEventFamily::Admin, - crate::MdEventFamily::Lifecycle, - crate::MdEventFamily::Audit, -]; -const RISK_ENTRIES: &[&str] = &[ - "approve", - "approve_checked", - "revoke", - "freeze_account", - "thaw_account", - "set_authority", - "initialize_multisig", - "initialize_multisig2", - "initialize_default_account_state", - "update_default_account_state", - "enable_required_transfer_memos", - "disable_required_transfer_memos", - "enable_cpi_guard", - "disable_cpi_guard", - "initialize_non_transferable_mint", - "initialize_pausable_config", - "pause", - "resume", - "initialize_permissioned_burn", - "enable_confidential_credits", - "disable_confidential_credits", - "enable_non_confidential_credits", - "disable_non_confidential_credits", -]; - /// Stable fact-only classic SPL Token risk materializer. #[derive(Clone, Debug, Default)] pub struct MtRiskMaterializer; impl crate::MtMaterializer for crate::MtRiskMaterializer { fn materializer_name(&self) -> &'static str { - return "kb-lib.materializer.risk"; + return crate::MT_RISK_COMPONENT_NAME; } fn materializer_version(&self) -> &'static str { return env!("CARGO_PKG_VERSION"); @@ -66,7 +33,7 @@ impl crate::MtApiEventMaterializer for crate::MtRiskMaterializer { }; } fn accepted_families(&self) -> &'static [crate::MdEventFamily] { - return ACCEPTED_FAMILIES; + return crate::MT_RISK_ACCEPTED_FAMILIES; } fn accepts_observation(&self, observation: &crate::DcApiDecodedObservation) -> bool { return accepts_event(&observation.event); @@ -171,8 +138,9 @@ fn accepts_event(event: &crate::MdDecodedProtocolEvent) -> bool { && event.surface_code.0 == "spl.token") || (event.program_id.0 == kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID && event.surface_code.0 == "spl.token_2022"); - let token_risk = token_program && RISK_ENTRIES.contains(&event.event_name.0.as_str()); - return ACCEPTED_FAMILIES.contains(&event.event_family) + let token_risk = + token_program && crate::MT_RISK_RISK_ENTRIES.contains(&event.event_name.0.as_str()); + return crate::MT_RISK_ACCEPTED_FAMILIES.contains(&event.event_family) && (token_risk || is_ata_recovery(event)); } diff --git a/kb-lib/src/materializer/routing.rs b/kb-lib/src/materializer/routing.rs index 7312e48..fb1d3ee 100644 --- a/kb-lib/src/materializer/routing.rs +++ b/kb-lib/src/materializer/routing.rs @@ -1,9 +1,17 @@ // file: kb-lib/src/materializer/routing.rs -// version: 3 +// version: 4 //! `routing` materializer family. -mod core; +mod constants; +mod materializer; /// Reserved routing materializer. -pub use self::core::MtRoutingMaterializer; +pub use self::materializer::MtRoutingMaterializer; + +/// Empty accepted-family boundary for the reserved component. +pub(crate) use self::constants::MT_ROUTING_ACCEPTED_FAMILIES; +/// Stable runtime component name for the `routing` materializer. +pub(crate) use self::constants::MT_ROUTING_COMPONENT_NAME; +/// Stable processor name for the `routing` materializer. +pub(crate) use self::constants::MT_ROUTING_PROCESSOR_NAME; diff --git a/kb-lib/src/materializer/routing/constants.rs b/kb-lib/src/materializer/routing/constants.rs new file mode 100644 index 0000000..158345f --- /dev/null +++ b/kb-lib/src/materializer/routing/constants.rs @@ -0,0 +1,11 @@ +// file: kb-lib/src/materializer/routing/constants.rs +// version: 1 + +//! Local constants for the routing materializer. + +/// Stable runtime component name for the routing materializer. +pub(crate) const MT_ROUTING_COMPONENT_NAME: &str = "kb-lib.materializer.routing"; +/// Stable processor name persisted by the replay ledger. +pub(crate) const MT_ROUTING_PROCESSOR_NAME: &str = "materializer.routing"; +/// Empty family boundary retained while the materializer is reserved. +pub(crate) const MT_ROUTING_ACCEPTED_FAMILIES: &[crate::MdEventFamily] = &[]; diff --git a/kb-lib/src/materializer/routing/core.rs b/kb-lib/src/materializer/routing/materializer.rs similarity index 72% rename from kb-lib/src/materializer/routing/core.rs rename to kb-lib/src/materializer/routing/materializer.rs index 9a4854f..acfabcc 100644 --- a/kb-lib/src/materializer/routing/core.rs +++ b/kb-lib/src/materializer/routing/materializer.rs @@ -1,5 +1,5 @@ -// file: kb-lib/src/materializer/routing/core.rs -// version: 4 +// file: kb-lib/src/materializer/routing/materializer.rs +// version: 5 //! Reserved routing materializer. @@ -9,7 +9,7 @@ pub struct MtRoutingMaterializer; impl crate::MtMaterializer for crate::MtRoutingMaterializer { fn materializer_name(&self) -> &'static str { - return "kb-lib.materializer.routing"; + return crate::MT_ROUTING_COMPONENT_NAME; } fn materializer_version(&self) -> &'static str { @@ -22,27 +22,22 @@ impl crate::MtMaterializer for crate::MtRoutingMaterializer { fn materialize_event( &self, - event: &crate::MdDecodedProtocolEvent, + _event: &crate::MdDecodedProtocolEvent, ) -> kb_core::Result> { - return std::result::Result::Ok(std::vec![crate::MdMaterializedEvent { - signature: event.signature.clone(), - slot: event.slot, - decoded_family: event.event_family, - materialized_family: crate::MdMaterializedEventFamily::Routing, - }]); + return std::result::Result::Ok(std::vec::Vec::new()); } } impl crate::MtApiEventMaterializer for crate::MtRoutingMaterializer { fn identity(&self) -> crate::MtApiMaterializerIdentity { return crate::MtApiMaterializerIdentity { - name: "kb-lib.materializer.routing".to_string(), + name: crate::MT_ROUTING_PROCESSOR_NAME.to_string(), version: env!("CARGO_PKG_VERSION").to_string(), }; } fn accepted_families(&self) -> &'static [crate::MdEventFamily] { - return &[]; + return crate::MT_ROUTING_ACCEPTED_FAMILIES; } fn transaction_policy( diff --git a/kb-lib/src/materializer/staking.rs b/kb-lib/src/materializer/staking.rs index e0a55d2..3cf1cfb 100644 --- a/kb-lib/src/materializer/staking.rs +++ b/kb-lib/src/materializer/staking.rs @@ -1,13 +1,45 @@ // file: kb-lib/src/materializer/staking.rs -// version: 4 +// version: 5 //! `staking` materializer family. mod constants; -mod core; +mod materializer; /// Stable native stake and vote materializer. -pub use self::core::MtStakingMaterializer; +pub use self::materializer::MtStakingMaterializer; -/// Canonical tracing target for the native stake and vote materializer. +/// Crate-visible `MT_STAKING_ACCEPTED_FAMILIES` component constant. +pub(crate) use self::constants::MT_STAKING_ACCEPTED_FAMILIES; +/// Stable runtime component name for the `staking` materializer. +pub(crate) use self::constants::MT_STAKING_COMPONENT_NAME; +/// Stable processor name for the `staking` materializer. +pub(crate) use self::constants::MT_STAKING_PROCESSOR_NAME; +/// Stable projection version for the `staking` materializer. +pub(crate) use self::constants::MT_STAKING_PROJECTION_VERSION; +/// Crate-visible `MT_STAKING_STAKE_ACCOUNT_ENTRIES` component constant. +pub(crate) use self::constants::MT_STAKING_STAKE_ACCOUNT_ENTRIES; +/// Crate-visible `MT_STAKING_STAKE_AUTHORITY_ENTRIES` component constant. +pub(crate) use self::constants::MT_STAKING_STAKE_AUTHORITY_ENTRIES; +/// Crate-visible `MT_STAKING_STAKE_LOCKUP_ENTRIES` component constant. +pub(crate) use self::constants::MT_STAKING_STAKE_LOCKUP_ENTRIES; +/// Crate-visible `MT_STAKING_STAKE_SURFACE` component constant. +pub(crate) use self::constants::MT_STAKING_STAKE_SURFACE; +/// Crate-visible `MT_STAKING_STAKE_VALUE_ENTRIES` component constant. +pub(crate) use self::constants::MT_STAKING_STAKE_VALUE_ENTRIES; +/// Crate-visible `MT_STAKING_VOTE_ACCOUNT_ENTRIES` component constant. +pub(crate) use self::constants::MT_STAKING_VOTE_ACCOUNT_ENTRIES; +/// Crate-visible `MT_STAKING_VOTE_ADMIN_ENTRIES` component constant. +pub(crate) use self::constants::MT_STAKING_VOTE_ADMIN_ENTRIES; +/// Crate-visible `MT_STAKING_VOTE_AUTHORITY_ENTRIES` component constant. +pub(crate) use self::constants::MT_STAKING_VOTE_AUTHORITY_ENTRIES; +/// Crate-visible `MT_STAKING_VOTE_REWARD_ENTRIES` component constant. +pub(crate) use self::constants::MT_STAKING_VOTE_REWARD_ENTRIES; +/// Crate-visible `MT_STAKING_VOTE_STATE_ENTRIES` component constant. +pub(crate) use self::constants::MT_STAKING_VOTE_STATE_ENTRIES; +/// Crate-visible `MT_STAKING_VOTE_SURFACE` component constant. +pub(crate) use self::constants::MT_STAKING_VOTE_SURFACE; +/// Crate-visible `MT_STAKING_VOTE_VALUE_ENTRIES` component constant. +pub(crate) use self::constants::MT_STAKING_VOTE_VALUE_ENTRIES; +/// Canonical tracing target for the `staking` materializer. pub(crate) use self::constants::TRACING_TARGET_MATERIALIZER_STAKING; diff --git a/kb-lib/src/materializer/staking/constants.rs b/kb-lib/src/materializer/staking/constants.rs index bd54a1e..b625d75 100644 --- a/kb-lib/src/materializer/staking/constants.rs +++ b/kb-lib/src/materializer/staking/constants.rs @@ -1,7 +1,77 @@ // file: kb-lib/src/materializer/staking/constants.rs -// version: 2 +// version: 4 -//! Local constants for the native stake and vote materializer. +//! Local constants for the staking materializer. -/// Canonical tracing target for this component. +/// Stable runtime component name for the staking materializer. +pub(crate) const MT_STAKING_COMPONENT_NAME: &str = "kb-lib.materializer.staking"; +/// Stable processor name persisted by the replay ledger. +pub(crate) const MT_STAKING_PROCESSOR_NAME: &str = "materializer.staking"; +/// Stable projection contract version for the staking materializer. +pub(crate) const MT_STAKING_PROJECTION_VERSION: u32 = 2; +/// Canonical tracing target for the staking materializer. pub(crate) const TRACING_TARGET_MATERIALIZER_STAKING: &str = "kb-lib.materializer.staking"; + +/// Component contract for `accepted_families`. +pub(crate) const MT_STAKING_ACCEPTED_FAMILIES: &[crate::MdEventFamily] = &[ + crate::MdEventFamily::Lifecycle, + crate::MdEventFamily::Admin, + crate::MdEventFamily::Staking, + crate::MdEventFamily::Reward, +]; +/// Component contract for `stake_surface`. +pub(crate) const MT_STAKING_STAKE_SURFACE: &str = "solana.core.stake"; +/// Component contract for `vote_surface`. +pub(crate) const MT_STAKING_VOTE_SURFACE: &str = "solana.core.vote"; +/// Component contract for `stake_account_entries`. +pub(crate) const MT_STAKING_STAKE_ACCOUNT_ENTRIES: &[&str] = &[ + "initialize", + "initialize_checked", + "delegate_stake", + "deactivate", + "deactivate_delinquent", +]; +/// Component contract for `stake_value_entries`. +pub(crate) const MT_STAKING_STAKE_VALUE_ENTRIES: &[&str] = + &["split", "withdraw", "merge", "move_stake", "move_lamports"]; +/// Component contract for `stake_authority_entries`. +pub(crate) const MT_STAKING_STAKE_AUTHORITY_ENTRIES: &[&str] = &[ + "authorize", + "authorize_with_seed", + "authorize_checked", + "authorize_checked_with_seed", +]; +/// Component contract for `stake_lockup_entries`. +pub(crate) const MT_STAKING_STAKE_LOCKUP_ENTRIES: &[&str] = &["set_lockup", "set_lockup_checked"]; +/// Component contract for `vote_account_entries`. +pub(crate) const MT_STAKING_VOTE_ACCOUNT_ENTRIES: &[&str] = + &["initialize_account", "initialize_account_v2"]; +/// Component contract for `vote_authority_entries`. +pub(crate) const MT_STAKING_VOTE_AUTHORITY_ENTRIES: &[&str] = &[ + "authorize", + "authorize_checked", + "authorize_with_seed", + "authorize_checked_with_seed", +]; +/// Component contract for `vote_admin_entries`. +pub(crate) const MT_STAKING_VOTE_ADMIN_ENTRIES: &[&str] = &[ + "update_validator_identity", + "update_commission", + "update_commission_collector", + "update_commission_bps", +]; +/// Component contract for `vote_state_entries`. +pub(crate) const MT_STAKING_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", +]; +/// Component contract for `vote_value_entries`. +pub(crate) const MT_STAKING_VOTE_VALUE_ENTRIES: &[&str] = &["withdraw"]; +/// Component contract for `vote_reward_entries`. +pub(crate) const MT_STAKING_VOTE_REWARD_ENTRIES: &[&str] = &["deposit_delegator_rewards"]; diff --git a/kb-lib/src/materializer/staking/core.rs b/kb-lib/src/materializer/staking/materializer.rs similarity index 92% rename from kb-lib/src/materializer/staking/core.rs rename to kb-lib/src/materializer/staking/materializer.rs index fb9e001..ed71b39 100644 --- a/kb-lib/src/materializer/staking/core.rs +++ b/kb-lib/src/materializer/staking/materializer.rs @@ -1,57 +1,8 @@ -// file: kb-lib/src/materializer/staking/core.rs -// version: 10 +// file: kb-lib/src/materializer/staking/materializer.rs +// version: 12 //! Instruction-level staking projections for native Stake and Vote observations. -const ACCEPTED_FAMILIES: &[crate::MdEventFamily] = &[ - crate::MdEventFamily::Lifecycle, - crate::MdEventFamily::Admin, - crate::MdEventFamily::Staking, - crate::MdEventFamily::Reward, -]; -const STAKE_SURFACE: &str = "solana.core.stake"; -const VOTE_SURFACE: &str = "solana.core.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"]; - #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum ProjectionKind { StakeAccount, @@ -100,7 +51,7 @@ pub struct MtStakingMaterializer; impl crate::MtMaterializer for crate::MtStakingMaterializer { fn materializer_name(&self) -> &'static str { - return "kb-lib.materializer.staking"; + return crate::MT_STAKING_COMPONENT_NAME; } fn materializer_version(&self) -> &'static str { @@ -126,13 +77,13 @@ impl crate::MtMaterializer for crate::MtStakingMaterializer { impl crate::MtApiEventMaterializer for crate::MtStakingMaterializer { fn identity(&self) -> crate::MtApiMaterializerIdentity { return crate::MtApiMaterializerIdentity { - name: "materializer.staking".to_string(), + name: crate::MT_STAKING_PROCESSOR_NAME.to_string(), version: env!("CARGO_PKG_VERSION").to_string(), }; } fn accepted_families(&self) -> &'static [crate::MdEventFamily] { - return ACCEPTED_FAMILIES; + return crate::MT_STAKING_ACCEPTED_FAMILIES; } fn accepts_observation(&self, observation: &crate::DcApiDecodedObservation) -> bool { @@ -205,7 +156,7 @@ impl crate::MtApiEventMaterializer for crate::MtStakingMaterializer { output_key: projection.output_key(operation.as_str()), family: projection.family(), payload_json: serde_json::json!({ - "projectionVersion": 1, + "projectionVersion": crate::MT_STAKING_PROJECTION_VERSION, "projectionSemantics": "committed_instruction_staking_event", "domain": projection.domain(), "operation": operation, @@ -214,6 +165,10 @@ impl crate::MtApiEventMaterializer for crate::MtStakingMaterializer { "slot": observation.event.slot.0, "instructionPath": observation.event.instruction_path.0.clone(), "transactionSucceeded": true, + "provenance": { + "processorName": crate::MT_STAKING_PROCESSOR_NAME, + "processorVersion": env!("CARGO_PKG_VERSION"), + }, "accounts": accounts, "parameters": parameters, "projection": projection_details( @@ -242,52 +197,54 @@ impl crate::MtApiEventMaterializer for crate::MtStakingMaterializer { } fn family_is_accepted(family: crate::MdEventFamily) -> bool { - return ACCEPTED_FAMILIES.iter().any(|accepted| return *accepted == family); + return crate::MT_STAKING_ACCEPTED_FAMILIES + .iter() + .any(|accepted| return *accepted == family); } fn projection(surface_code: &str, entry_code: &str) -> std::option::Option { - if surface_code == STAKE_SURFACE { + if surface_code == crate::MT_STAKING_STAKE_SURFACE { return stake_projection(entry_code); } - if surface_code == VOTE_SURFACE { + if surface_code == crate::MT_STAKING_VOTE_SURFACE { return vote_projection(entry_code); } return std::option::Option::None; } fn stake_projection(entry_code: &str) -> std::option::Option { - if contains(STAKE_ACCOUNT_ENTRIES, entry_code) { + if contains(crate::MT_STAKING_STAKE_ACCOUNT_ENTRIES, entry_code) { return std::option::Option::Some(ProjectionKind::StakeAccount); } - if contains(STAKE_VALUE_ENTRIES, entry_code) { + if contains(crate::MT_STAKING_STAKE_VALUE_ENTRIES, entry_code) { return std::option::Option::Some(ProjectionKind::StakeValue); } - if contains(STAKE_AUTHORITY_ENTRIES, entry_code) { + if contains(crate::MT_STAKING_STAKE_AUTHORITY_ENTRIES, entry_code) { return std::option::Option::Some(ProjectionKind::StakeAuthority); } - if contains(STAKE_LOCKUP_ENTRIES, entry_code) { + if contains(crate::MT_STAKING_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 { - if contains(VOTE_ACCOUNT_ENTRIES, entry_code) { + if contains(crate::MT_STAKING_VOTE_ACCOUNT_ENTRIES, entry_code) { return std::option::Option::Some(ProjectionKind::VoteAccount); } - if contains(VOTE_AUTHORITY_ENTRIES, entry_code) { + if contains(crate::MT_STAKING_VOTE_AUTHORITY_ENTRIES, entry_code) { return std::option::Option::Some(ProjectionKind::VoteAuthority); } - if contains(VOTE_ADMIN_ENTRIES, entry_code) { + if contains(crate::MT_STAKING_VOTE_ADMIN_ENTRIES, entry_code) { return std::option::Option::Some(ProjectionKind::VoteAdmin); } - if contains(VOTE_STATE_ENTRIES, entry_code) { + if contains(crate::MT_STAKING_VOTE_STATE_ENTRIES, entry_code) { return std::option::Option::Some(ProjectionKind::VoteState); } - if contains(VOTE_VALUE_ENTRIES, entry_code) { + if contains(crate::MT_STAKING_VOTE_VALUE_ENTRIES, entry_code) { return std::option::Option::Some(ProjectionKind::VoteValue); } - if contains(VOTE_REWARD_ENTRIES, entry_code) { + if contains(crate::MT_STAKING_VOTE_REWARD_ENTRIES, entry_code) { return std::option::Option::Some(ProjectionKind::VoteReward); } return std::option::Option::None; @@ -637,7 +594,7 @@ mod tests { fn stake_initialize_creates_instructional_staking_projection() { let materializer = crate::MtStakingMaterializer; let observation = observation( - super::STAKE_SURFACE, + crate::MT_STAKING_STAKE_SURFACE, "initialize", crate::MdEventFamily::Lifecycle, false, @@ -659,7 +616,7 @@ mod tests { fn stake_authority_and_lockup_events_are_projected() { let materializer = crate::MtStakingMaterializer; let authority = observation( - super::STAKE_SURFACE, + crate::MT_STAKING_STAKE_SURFACE, "authorize_with_seed", crate::MdEventFamily::Admin, false, @@ -672,7 +629,7 @@ mod tests { }), ); let lockup = observation( - super::STAKE_SURFACE, + crate::MT_STAKING_STAKE_SURFACE, "set_lockup_checked", crate::MdEventFamily::Admin, false, @@ -701,7 +658,7 @@ mod tests { fn stake_value_projection_does_not_duplicate_lamport_changes() { let materializer = crate::MtStakingMaterializer; let observation = observation( - super::STAKE_SURFACE, + crate::MT_STAKING_STAKE_SURFACE, "withdraw", crate::MdEventFamily::Staking, false, @@ -724,7 +681,7 @@ mod tests { fn vote_account_and_authority_events_are_projected() { let materializer = crate::MtStakingMaterializer; let initialize = observation( - super::VOTE_SURFACE, + crate::MT_STAKING_VOTE_SURFACE, "initialize_account_v2", crate::MdEventFamily::Lifecycle, false, @@ -739,7 +696,7 @@ mod tests { }), ); let authorize = observation( - super::VOTE_SURFACE, + crate::MT_STAKING_VOTE_SURFACE, "authorize_checked", crate::MdEventFamily::Admin, false, @@ -769,7 +726,7 @@ mod tests { fn vote_state_and_reward_outputs_are_distinguished() { let materializer = crate::MtStakingMaterializer; let vote_state = observation( - super::VOTE_SURFACE, + crate::MT_STAKING_VOTE_SURFACE, "tower_sync_switch", crate::MdEventFamily::Staking, false, @@ -780,7 +737,7 @@ mod tests { serde_json::json!({"root": 10, "hash": "hash111", "switchProofHash": "switch111"}), ); let reward = observation( - super::VOTE_SURFACE, + crate::MT_STAKING_VOTE_SURFACE, "deposit_delegator_rewards", crate::MdEventFamily::Reward, false, @@ -805,7 +762,7 @@ mod tests { fn failed_staking_mutation_is_refused() { let materializer = crate::MtStakingMaterializer; let observation = observation( - super::STAKE_SURFACE, + crate::MT_STAKING_STAKE_SURFACE, "delegate_stake", crate::MdEventFamily::Staking, true, @@ -829,7 +786,7 @@ mod tests { serde_json::json!({}), ); let redelegate = observation( - super::STAKE_SURFACE, + crate::MT_STAKING_STAKE_SURFACE, "redelegate", crate::MdEventFamily::Staking, false, @@ -848,7 +805,7 @@ mod tests { fn output_serialization_is_deterministic() { let materializer = crate::MtStakingMaterializer; let observation = observation( - super::VOTE_SURFACE, + crate::MT_STAKING_VOTE_SURFACE, "update_commission_bps", crate::MdEventFamily::Admin, false, diff --git a/kb-lib/src/materializer/token.rs b/kb-lib/src/materializer/token.rs index 43a0cc0..720326c 100644 --- a/kb-lib/src/materializer/token.rs +++ b/kb-lib/src/materializer/token.rs @@ -1,10 +1,9 @@ // file: kb-lib/src/materializer/token.rs -// version: 6 +// version: 7 //! `token` materializer family. mod accounts; -mod constants; mod metadata_risk; /// Stable Token, ATA and Token-2022 account materializer. @@ -14,9 +13,23 @@ pub use self::accounts::materializer_token_materialize_token_2022_state_snapshot /// Reserved token-metadata risk materializer. pub use self::metadata_risk::MtTokenMetadataRiskMaterializer; +/// Crate-visible `MT_TOKEN_ACCOUNTS_ACCEPTED_FAMILIES` component constant. +pub(crate) use self::accounts::MT_TOKEN_ACCOUNTS_ACCEPTED_FAMILIES; +/// Crate-visible `MT_TOKEN_ACCOUNTS_ATA_LIFECYCLE_ENTRIES` component constant. +pub(crate) use self::accounts::MT_TOKEN_ACCOUNTS_ATA_LIFECYCLE_ENTRIES; +/// Stable runtime component name for Token, ATA and Token-2022 account projections. +pub(crate) use self::accounts::MT_TOKEN_ACCOUNTS_COMPONENT_NAME; +/// Crate-visible `MT_TOKEN_ACCOUNTS_MUTATION_ENTRIES` component constant. +pub(crate) use self::accounts::MT_TOKEN_ACCOUNTS_MUTATION_ENTRIES; /// Stable processor name for Token, ATA and Token-2022 account projections. -pub(crate) use self::constants::MT_TOKEN_ACCOUNTS_PROCESSOR_NAME; +pub(crate) use self::accounts::MT_TOKEN_ACCOUNTS_PROCESSOR_NAME; /// Stable projection contract version for Token, ATA and Token-2022 account projections. -pub(crate) use self::constants::MT_TOKEN_ACCOUNTS_PROJECTION_VERSION; +pub(crate) use self::accounts::MT_TOKEN_ACCOUNTS_PROJECTION_VERSION; /// Canonical tracing target for Token, ATA and Token-2022 account projections. -pub(crate) use self::constants::TRACING_TARGET_MATERIALIZER_TOKEN_ACCOUNTS; +pub(crate) use self::accounts::TRACING_TARGET_MATERIALIZER_TOKEN_ACCOUNTS; +/// Empty accepted-family boundary for the reserved component. +pub(crate) use self::metadata_risk::MT_TOKEN_METADATA_RISK_ACCEPTED_FAMILIES; +/// Stable runtime component name for the reserved token-metadata risk materializer. +pub(crate) use self::metadata_risk::MT_TOKEN_METADATA_RISK_COMPONENT_NAME; +/// Stable processor name for the reserved token-metadata risk materializer. +pub(crate) use self::metadata_risk::MT_TOKEN_METADATA_RISK_PROCESSOR_NAME; diff --git a/kb-lib/src/materializer/token/accounts.rs b/kb-lib/src/materializer/token/accounts.rs index ef89aee..8957352 100644 --- a/kb-lib/src/materializer/token/accounts.rs +++ b/kb-lib/src/materializer/token/accounts.rs @@ -1,842 +1,27 @@ // file: kb-lib/src/materializer/token/accounts.rs -// version: 7 +// version: 8 -//! Stable committed SPL Token mutation and ATA lifecycle projections. +//! Token, ATA and Token-2022 account materializer component. -const ACCEPTED_FAMILIES: &[crate::MdEventFamily] = &[ - crate::MdEventFamily::TokenAccount, - crate::MdEventFamily::TokenMint, - crate::MdEventFamily::TokenBurn, - crate::MdEventFamily::Lifecycle, -]; -const MUTATION_ENTRIES: &[&str] = &[ - "initialize_mint", - "initialize_mint2", - "initialize_account", - "initialize_account2", - "initialize_account3", - "transfer", - "transfer_checked", - "approve", - "approve_checked", - "revoke", - "mint_to", - "mint_to_checked", - "burn", - "burn_checked", - "close_account", - "freeze_account", - "thaw_account", - "sync_native", - "initialize_immutable_owner", - "withdraw_excess_lamports", - "unwrap_lamports", - "initialize_non_transferable_mint", - "reallocate", -]; -const ATA_LIFECYCLE_ENTRIES: &[(&str, &str)] = &[ - ("create", "ata_created"), - ("create_idempotent", "ata_created_or_reused_idempotently"), - ("recover_nested", "nested_ata_recovered"), -]; +mod constants; +mod materializer; -/// Stable committed classic SPL Token account and mint mutation materializer. -#[derive(Clone, Debug, Default)] -pub struct MtTokenAccountsMaterializer; +/// Stable Token, ATA and Token-2022 account materializer. +pub use self::materializer::MtTokenAccountsMaterializer; +/// Materializes one bounded Token-2022 account state snapshot. +pub use self::materializer::materializer_token_materialize_token_2022_state_snapshot; -impl crate::MtMaterializer for crate::MtTokenAccountsMaterializer { - fn materializer_name(&self) -> &'static str { - return "kb-lib.materializer.token.accounts"; - } - - fn materializer_version(&self) -> &'static str { - return env!("CARGO_PKG_VERSION"); - } - - fn accepts_event(&self, event: &crate::MdDecodedProtocolEvent) -> bool { - return accepts_event(event); - } - - fn materialize_event( - &self, - _event: &crate::MdDecodedProtocolEvent, - ) -> kb_core::Result> { - return std::result::Result::Ok(std::vec::Vec::new()); - } -} - -impl crate::MtApiEventMaterializer for crate::MtTokenAccountsMaterializer { - fn identity(&self) -> crate::MtApiMaterializerIdentity { - return crate::MtApiMaterializerIdentity { - name: crate::MT_TOKEN_ACCOUNTS_PROCESSOR_NAME.to_string(), - version: env!("CARGO_PKG_VERSION").to_string(), - }; - } - - fn accepted_families(&self) -> &'static [crate::MdEventFamily] { - return ACCEPTED_FAMILIES; - } - - fn accepts_observation(&self, observation: &crate::DcApiDecodedObservation) -> bool { - return accepts_event(&observation.event); - } - - fn transaction_policy( - &self, - _family: crate::MdEventFamily, - ) -> crate::MtApiMaterializationTransactionPolicy { - return crate::MtApiMaterializationTransactionPolicy::SuccessfulCommittedOnly; - } - - fn materialize( - &self, - observation: &crate::DcApiDecodedObservation, - ) -> crate::MtApiMaterializerExecutionResult { - if !accepts_event(&observation.event) { - return crate::MtApiMaterializerExecutionResult::ignored(); - } - if observation.transaction_failed || !observation.observation_committed { - return crate::MtApiMaterializerExecutionResult::refused( - "uncommitted_token_account_projection_refused", - "failed or uncommitted SPL Token or ATA observations cannot create mutable outputs", - ); - } - if is_ata_event(&observation.event) { - return materialize_ata(observation); - } - let parameters = observation - .payload_json - .get("parameters") - .cloned() - .unwrap_or(serde_json::Value::Null); - let accounts = observation - .payload_json - .get("accounts") - .cloned() - .unwrap_or(serde_json::Value::Null); - let authority = observation - .payload_json - .get("authority") - .cloned() - .unwrap_or(serde_json::Value::Null); - let operation = observation.event.event_name.0.clone(); - let output = crate::MtApiMaterializedOutput { - output_key: format!("token_account_mutation:{operation}:0"), - family: crate::MdMaterializedEventFamily::TokenAccount, - payload_json: serde_json::json!({ - "projectionVersion": crate::MT_TOKEN_ACCOUNTS_PROJECTION_VERSION, - "domain": "spl_token_account_mutation", - "projectionSemantics": "committed_instruction_mutation_not_final_account_snapshot", - "idempotenceKey": format!( - "spl_token:{}:{}:{}", - observation.event.signature.0, - observation.event.instruction_path.0, - operation - ), - "operation": operation, - "signature": observation.event.signature.0.clone(), - "slot": observation.event.slot.0, - "instructionPath": observation.event.instruction_path.0.clone(), - "programId": observation.event.program_id.0.clone(), - "mint": account_key_for_role(&accounts, "mint"), - "amountRaw": parameters.get("amountRaw").cloned().unwrap_or(serde_json::Value::Null), - "decimals": parameters.get("decimals").cloned().unwrap_or(serde_json::Value::Null), - "parameters": parameters, - "accounts": accounts, - "authority": authority, - "committed": true, - "transactionFinalAccountStateCaptured": false, - "coreBalanceChangesDuplicated": false, - "provenance": { - "processorName": crate::MT_TOKEN_ACCOUNTS_PROCESSOR_NAME, - "processorVersion": env!("CARGO_PKG_VERSION"), - "sourceSurface": observation.event.surface_code.0.clone(), - "sourceEventCode": observation.event.event_code.0.clone(), - "sourceEventKey": observation.event_key.clone() - } - }), - }; - tracing::debug!( - target: crate::TRACING_TARGET_MATERIALIZER_TOKEN_ACCOUNTS, - action = "materialize_token_account_mutation", - signature = %observation.event.signature.0, - instruction_path = %observation.event.instruction_path.0, - operation = %observation.event.event_name.0, - "materialize committed classic SPL Token mutation" - ); - return crate::MtApiMaterializerExecutionResult { - status: crate::MtApiMaterializerOutcomeStatus::Inserted, - outputs: std::vec![output], - diagnostics: std::vec::Vec::new(), - }; - } -} - -fn accepts_event(event: &crate::MdDecodedProtocolEvent) -> bool { - let token_program = (event.program_id.0 == kb_program_ids::SPL_TOKEN_PROGRAM_ID - && event.surface_code.0 == "spl.token") - || (event.program_id.0 == kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID - && event.surface_code.0 == "spl.token_2022"); - let token_mutation = token_program && MUTATION_ENTRIES.contains(&event.event_name.0.as_str()); - let ata = is_ata_event(event); - return ACCEPTED_FAMILIES.contains(&event.event_family) && (token_mutation || ata); -} - -fn is_ata_event(event: &crate::MdDecodedProtocolEvent) -> bool { - return event.program_id.0 == kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID - && event.surface_code.0 == "spl.associated_token_account" - && event.event_family == crate::MdEventFamily::Lifecycle - && ATA_LIFECYCLE_ENTRIES - .iter() - .any(|(entry, _)| return *entry == event.event_name.0); -} - -fn materialize_ata( - observation: &crate::DcApiDecodedObservation, -) -> crate::MtApiMaterializerExecutionResult { - let operation = observation.event.event_name.0.as_str(); - let addresses = match observation.payload_json.get("addresses") { - std::option::Option::Some(value) if value.is_object() => value, - _ => { - return crate::MtApiMaterializerExecutionResult::refused( - "ata_addresses_missing", - "committed ATA lifecycle projection requires the decoded address contract", - ); - }, - }; - let validation = validate_ata_addresses(operation, addresses); - if let std::result::Result::Err((code, message)) = validation { - return crate::MtApiMaterializerExecutionResult::refused(code, message); - } - let lifecycle_kind = - match ATA_LIFECYCLE_ENTRIES.iter().find(|(entry, _)| return *entry == operation) { - std::option::Option::Some((_, value)) => *value, - std::option::Option::None => { - return crate::MtApiMaterializerExecutionResult::ignored(); - }, - }; - let associated_token_account = if operation == "recover_nested" { - observed_address(addresses, "nestedAssociatedTokenAccount") - } else { - observed_address(addresses, "associatedTokenAccount") - }; - let token_program = addresses - .get("tokenProgram") - .and_then(|value| return value.get("programId")) - .cloned() - .unwrap_or(serde_json::Value::Null); - let wallet = addresses.get("walletOwner").cloned().unwrap_or(serde_json::Value::Null); - let mint = if operation == "recover_nested" { - addresses.get("nestedMint").cloned().unwrap_or(serde_json::Value::Null) - } else { - addresses.get("mint").cloned().unwrap_or(serde_json::Value::Null) - }; - let output = crate::MtApiMaterializedOutput { - output_key: format!("ata_lifecycle:{lifecycle_kind}:0"), - family: crate::MdMaterializedEventFamily::TokenAccount, - payload_json: serde_json::json!({ - "projectionVersion": crate::MT_TOKEN_ACCOUNTS_PROJECTION_VERSION, - "domain": "spl_associated_token_account_lifecycle", - "projectionSemantics": "committed_associated_address_lifecycle_not_token_state_snapshot", - "idempotenceKey": format!( - "ata:{}:{}:{}:{}:{}", - observation.event.signature.0, - observation.event.instruction_path.0, - observation.event.program_id.0, - lifecycle_kind, - associated_token_account.as_str().unwrap_or("") - ), - "operation": operation, - "lifecycleKind": lifecycle_kind, - "associatedTokenAccount": associated_token_account, - "walletOwner": wallet, - "mint": mint, - "tokenProgramId": token_program, - "fundingAccount": addresses.get("fundingAccount").cloned().unwrap_or(serde_json::Value::Null), - "ownerMint": addresses.get("ownerMint").cloned().unwrap_or(serde_json::Value::Null), - "nestedMint": addresses.get("nestedMint").cloned().unwrap_or(serde_json::Value::Null), - "ownerAssociatedTokenAccount": observed_address(addresses, "ownerAssociatedTokenAccount"), - "nestedAssociatedTokenAccount": observed_address(addresses, "nestedAssociatedTokenAccount"), - "walletNestedMintAssociatedTokenAccount": observed_address(addresses, "walletNestedMintAssociatedTokenAccount"), - "idempotentOutcome": if operation == "create_idempotent" { - "created_or_reused_not_distinguishable_from_parent_instruction_alone" - } else { - "not_applicable" - }, - "signature": observation.event.signature.0.clone(), - "slot": observation.event.slot.0, - "instructionPath": observation.event.instruction_path.0.clone(), - "programId": observation.event.program_id.0.clone(), - "committed": true, - "transactionFinalAccountStateCaptured": false, - "tokenBalanceReconstructed": false, - "splTokenCpiMutationsDuplicated": false, - "ownership": { - "ataParent": "associated_address_lifecycle_only", - "splTokenCpi": "initialize_transfer_and_close_mutations" - }, - "provenance": { - "processorName": crate::MT_TOKEN_ACCOUNTS_PROCESSOR_NAME, - "processorVersion": env!("CARGO_PKG_VERSION"), - "sourceSurface": observation.event.surface_code.0.clone(), - "sourceEventCode": observation.event.event_code.0.clone(), - "sourceEventKey": observation.event_key.clone() - } - }), - }; - tracing::debug!( - target: crate::TRACING_TARGET_MATERIALIZER_TOKEN_ACCOUNTS, - action = "materialize_ata_lifecycle", - signature = %observation.event.signature.0, - instruction_path = %observation.event.instruction_path.0, - operation, - lifecycle_kind, - "materialize committed ATA lifecycle without duplicating SPL Token CPI mutations" - ); - return crate::MtApiMaterializerExecutionResult { - status: crate::MtApiMaterializerOutcomeStatus::Inserted, - outputs: std::vec![output], - diagnostics: std::vec::Vec::new(), - }; -} - -fn validate_ata_addresses( - operation: &str, - addresses: &serde_json::Value, -) -> std::result::Result<(), (&'static str, &'static str)> { - let token_supported = addresses - .get("tokenProgram") - .and_then(|value| return value.get("supported")) - .and_then(serde_json::Value::as_bool) - == std::option::Option::Some(true); - let token_program_id = addresses - .get("tokenProgram") - .and_then(|value| return value.get("programId")) - .and_then(serde_json::Value::as_str); - let token_program_exact = matches!( - token_program_id, - std::option::Option::Some(kb_program_ids::SPL_TOKEN_PROGRAM_ID) - | std::option::Option::Some(kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID) - ); - if !token_supported || !token_program_exact { - return std::result::Result::Err(( - "ata_token_program_not_supported", - "ATA lifecycle projection requires classic SPL Token or Token-2022", - )); - } - let fields: &[&str] = if operation == "recover_nested" { - &[ - "ownerAssociatedTokenAccount", - "nestedAssociatedTokenAccount", - "walletNestedMintAssociatedTokenAccount", - ] - } else { - &["associatedTokenAccount"] - }; - for field in fields { - let valid = addresses - .get(*field) - .and_then(|value| return value.get("valid")) - .and_then(serde_json::Value::as_bool) - == std::option::Option::Some(true); - let observed = observed_address(addresses, field); - if !valid || observed.as_str().is_none_or(|value| return value.is_empty()) { - return std::result::Result::Err(( - "ata_derived_address_invalid", - "ATA lifecycle projection requires every canonical address validation to pass", - )); - } - } - let identity_fields: &[&str] = if operation == "recover_nested" { - &["walletOwner", "ownerMint", "nestedMint"] - } else { - &["walletOwner", "mint"] - }; - if identity_fields.iter().any(|field| { - return addresses - .get(*field) - .and_then(serde_json::Value::as_str) - .is_none_or(|value| return value.is_empty()); - }) { - return std::result::Result::Err(( - "ata_identity_field_missing", - "ATA lifecycle projection requires wallet and mint identities", - )); - } - return std::result::Result::Ok(()); -} - -fn observed_address(addresses: &serde_json::Value, field: &str) -> serde_json::Value { - return addresses - .get(field) - .and_then(|value| return value.get("observed")) - .cloned() - .unwrap_or(serde_json::Value::Null); -} - -fn account_key_for_role(accounts: &serde_json::Value, role: &str) -> serde_json::Value { - let value = accounts.as_array().and_then(|rows| { - return rows.iter().find_map(|row| { - if row.get("role").and_then(serde_json::Value::as_str) - != std::option::Option::Some(role) - { - return std::option::Option::None; - } - return row.get("accountKey").and_then(serde_json::Value::as_str); - }); - }); - return match value { - std::option::Option::Some(account_key) => { - serde_json::Value::String(account_key.to_string()) - }, - std::option::Option::None => serde_json::Value::Null, - }; -} - -/// Materialize one already parsed Token-2022 account snapshot without inferring hidden state. -pub fn materializer_token_materialize_token_2022_state_snapshot( - account_key: &str, - slot: u64, - state: &crate::DcToken2022State, -) -> std::result::Result { - if account_key.is_empty() { - return std::result::Result::Err( - "Token-2022 state snapshot requires a non-empty account key".to_string(), - ); - } - let kind = match state.kind { - crate::DcToken2022StateKind::Mint => "mint", - crate::DcToken2022StateKind::Account => "account", - crate::DcToken2022StateKind::Multisig => "multisig", - }; - let extensions = state - .extensions - .iter() - .map(|entry| { - return serde_json::json!({ - "extensionType": entry.extension_type, - "extensionName": entry.extension_name, - "valueHex": entry.value_hex, - "valueFields": entry.value_fields, - }); - }) - .collect::>(); - let extension_names = state - .extensions - .iter() - .map(|entry| return entry.extension_name) - .collect::>(); - let contains_confidential_state = state.extensions.iter().any(|entry| { - return entry.extension_name.starts_with("confidential_") - || entry.extension_name == "confidential_mint_burn"; - }); - return std::result::Result::Ok(crate::MtApiMaterializedOutput { - output_key: format!("token_2022_state_snapshot:{account_key}:{slot}"), - family: crate::MdMaterializedEventFamily::TokenAccount, - payload_json: serde_json::json!({ - "projectionVersion": crate::MT_TOKEN_ACCOUNTS_PROJECTION_VERSION, - "domain": "spl_token_2022_account_state", - "projectionSemantics": "authoritative_bounded_account_snapshot", - "idempotenceKey": format!("token_2022_state:{account_key}:{slot}"), - "accountKey": account_key, - "slot": slot, - "programId": kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID, - "stateKind": kind, - "baseFields": state.base_fields, - "baseHex": state.base_hex, - "accountType": state.account_type, - "extensionCount": state.extensions.len(), - "extensionNames": extension_names, - "extensions": extensions, - "containsConfidentialState": contains_confidential_state, - "confidentialValuesDecrypted": false, - "finalAccountStateCaptured": true, - "provenance": { - "processorName": crate::MT_TOKEN_ACCOUNTS_PROCESSOR_NAME, - "processorVersion": env!("CARGO_PKG_VERSION"), - "source": "bounded_token_2022_state_parser" - } - }), - }); -} - -#[cfg(test)] -mod tests { - fn observation( - entry: &str, - family: crate::MdEventFamily, - failed: bool, - ) -> crate::DcApiDecodedObservation { - return crate::DcApiDecodedObservation { - event_key: format!("token:{entry}:0"), - event: crate::MdDecodedProtocolEvent { - signature: crate::MdSignature("signature".to_string()), - slot: crate::MdSlot(42), - instruction_path: crate::MdInstructionPath("1/0".to_string()), - program_id: crate::MdProgramId(kb_program_ids::SPL_TOKEN_PROGRAM_ID.to_string()), - protocol_code: crate::MdProtocolCode("spl.token".to_string()), - surface_code: crate::MdSurfaceCode("spl.token".to_string()), - event_code: crate::MdEventCode(format!("spl.token.{entry}")), - event_name: crate::MdEventName(entry.to_string()), - event_family: family, - source_kind: crate::MdEventSourceKind::InnerInstruction, - confidence: crate::MdDecoderConfidence::ManualExact, - }, - payload_json: serde_json::json!({ - "parameters": {"amountRaw":"18446744073709551615","decimals":9}, - "accounts": [{"role":"mint","accountKey":"mint111"}], - "authority": {"form":"single"} - }), - transaction_failed: failed, - transaction_error: if failed { - std::option::Option::Some(serde_json::json!({"error":"failed"})) - } else { - std::option::Option::None - }, - observation_committed: !failed, - proof: crate::DcApiDecoderProof { - kind: crate::DcApiDecoderProofKind::Manual, - confidence: crate::MdDecoderConfidence::ManualExact, - evidence: std::vec!["fixture".to_string()], - }, - }; - } - - fn token_2022_observation( - entry: &str, - family: crate::MdEventFamily, - failed: bool, - ) -> crate::DcApiDecodedObservation { - let mut observation = observation(entry, family, failed); - observation.event.program_id = - crate::MdProgramId(kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID.to_string()); - observation.event.protocol_code = crate::MdProtocolCode("spl.token_2022".to_string()); - observation.event.surface_code = crate::MdSurfaceCode("spl.token_2022".to_string()); - observation.event.event_code = crate::MdEventCode(format!("spl.token_2022.{entry}")); - observation.event_key = format!("token_2022:{entry}:0"); - return observation; - } - - fn ata_observation(entry: &str, failed: bool, valid: bool) -> crate::DcApiDecodedObservation { - let addresses = if entry == "recover_nested" { - serde_json::json!({ - "tokenProgram": { - "programId": kb_program_ids::SPL_TOKEN_PROGRAM_ID, - "kind": "spl_token_classic", - "supported": true - }, - "ownerAssociatedTokenAccount": {"expected":"ownerAta","observed":"ownerAta","valid":valid}, - "nestedAssociatedTokenAccount": {"expected":"nestedAta","observed":"nestedAta","valid":valid}, - "walletNestedMintAssociatedTokenAccount": {"expected":"destinationAta","observed":"destinationAta","valid":valid}, - "walletOwner": "wallet111", - "ownerMint": "ownerMint111", - "nestedMint": "nestedMint111" - }) - } else { - serde_json::json!({ - "tokenProgram": { - "programId": kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID, - "kind": "token_2022", - "supported": true - }, - "associatedTokenAccount": {"expected":"ata111","observed":"ata111","valid":valid}, - "fundingAccount": "payer111", - "walletOwner": "wallet111", - "mint": "mint111" - }) - }; - return crate::DcApiDecodedObservation { - event_key: format!("ata:{entry}:0"), - event: crate::MdDecodedProtocolEvent { - signature: crate::MdSignature("signature".to_string()), - slot: crate::MdSlot(42), - instruction_path: crate::MdInstructionPath("0".to_string()), - program_id: crate::MdProgramId( - kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID.to_string(), - ), - protocol_code: crate::MdProtocolCode("spl.associated_token_account".to_string()), - surface_code: crate::MdSurfaceCode("spl.associated_token_account".to_string()), - event_code: crate::MdEventCode(format!("spl.associated_token_account.{entry}")), - event_name: crate::MdEventName(entry.to_string()), - event_family: crate::MdEventFamily::Lifecycle, - source_kind: crate::MdEventSourceKind::Instruction, - confidence: crate::MdDecoderConfidence::ManualExact, - }, - payload_json: serde_json::json!({"addresses":addresses}), - transaction_failed: failed, - transaction_error: std::option::Option::None, - observation_committed: !failed, - proof: crate::DcApiDecoderProof { - kind: crate::DcApiDecoderProofKind::Manual, - confidence: crate::MdDecoderConfidence::ManualExact, - evidence: std::vec!["fixture".to_string()], - }, - }; - } - - #[test] - fn committed_checked_transfer_preserves_exact_amount_mint_and_path() { - let observation = - observation("transfer_checked", crate::MdEventFamily::TokenAccount, false); - let result = crate::MtApiEventMaterializer::materialize( - &crate::MtTokenAccountsMaterializer, - &observation, - ); - assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted); - assert_eq!(result.outputs[0].payload_json["amountRaw"], "18446744073709551615"); - assert_eq!(result.outputs[0].payload_json["mint"], "mint111"); - assert_eq!(result.outputs[0].payload_json["instructionPath"], "1/0"); - assert_eq!(result.outputs[0].payload_json["transactionFinalAccountStateCaptured"], false); - } - - #[test] - fn committed_token_2022_transfer_has_the_same_single_projection_owner() { - let observation = - token_2022_observation("transfer_checked", crate::MdEventFamily::TokenAccount, false); - let result = crate::MtApiEventMaterializer::materialize( - &crate::MtTokenAccountsMaterializer, - &observation, - ); - assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted); - assert_eq!(result.outputs.len(), 1); - assert_eq!(result.outputs[0].payload_json["operation"], "transfer_checked"); - assert_eq!( - result.outputs[0].payload_json["programId"], - kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID - ); - } - - #[test] - fn failed_mutation_is_refused() { - let observation = observation("burn", crate::MdEventFamily::TokenBurn, true); - let result = crate::MtApiEventMaterializer::materialize( - &crate::MtTokenAccountsMaterializer, - &observation, - ); - assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Refused); - assert!(result.outputs.is_empty()); - } - - #[test] - fn batch_parent_admin_and_conversion_events_are_not_duplicated() { - for (entry, family) in [ - ("batch", crate::MdEventFamily::Audit), - ("set_authority", crate::MdEventFamily::Admin), - ("amount_to_ui_amount", crate::MdEventFamily::Audit), - ] { - let observation = observation(entry, family, false); - let result = crate::MtApiEventMaterializer::materialize( - &crate::MtTokenAccountsMaterializer, - &observation, - ); - assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Ignored); - } - } - - #[test] - fn every_ata_variant_has_one_exact_lifecycle_projection() { - for (entry, expected_kind, expected_ata, expected_token_program) in [ - ("create", "ata_created", "ata111", kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID), - ( - "create_idempotent", - "ata_created_or_reused_idempotently", - "ata111", - kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID, - ), - ( - "recover_nested", - "nested_ata_recovered", - "nestedAta", - kb_program_ids::SPL_TOKEN_PROGRAM_ID, - ), - ] { - let result = crate::MtApiEventMaterializer::materialize( - &crate::MtTokenAccountsMaterializer, - &ata_observation(entry, false, true), - ); - assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted); - assert_eq!(result.outputs.len(), 1); - assert_eq!(result.outputs[0].payload_json["lifecycleKind"], expected_kind); - assert_eq!(result.outputs[0].payload_json["associatedTokenAccount"], expected_ata); - assert_eq!(result.outputs[0].payload_json["tokenProgramId"], expected_token_program); - assert_eq!(result.outputs[0].payload_json["tokenBalanceReconstructed"], false); - assert_eq!(result.outputs[0].payload_json["splTokenCpiMutationsDuplicated"], false); - } - } - - #[test] - fn idempotent_parent_does_not_invent_created_or_reused_branch() { - let result = crate::MtApiEventMaterializer::materialize( - &crate::MtTokenAccountsMaterializer, - &ata_observation("create_idempotent", false, true), - ); - assert_eq!( - result.outputs[0].payload_json["idempotentOutcome"], - "created_or_reused_not_distinguishable_from_parent_instruction_alone" - ); - assert!(result.outputs[0].payload_json.get("reused").is_none()); - assert!(result.outputs[0].payload_json.get("created").is_none()); - } - - #[test] - fn uncommitted_or_invalid_ata_lifecycle_is_refused() { - for observation in [ - ata_observation("create", true, true), - ata_observation("recover_nested", false, false), - ] { - let result = crate::MtApiEventMaterializer::materialize( - &crate::MtTokenAccountsMaterializer, - &observation, - ); - assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Refused); - assert!(result.outputs.is_empty()); - } - } - - #[test] - fn ata_parent_and_classic_token_cpi_children_own_distinct_facts() { - let parent = crate::MtApiEventMaterializer::materialize( - &crate::MtTokenAccountsMaterializer, - &ata_observation("recover_nested", false, true), - ); - let transfer = crate::MtApiEventMaterializer::materialize( - &crate::MtTokenAccountsMaterializer, - &observation("transfer", crate::MdEventFamily::TokenAccount, false), - ); - let close = crate::MtApiEventMaterializer::materialize( - &crate::MtTokenAccountsMaterializer, - &observation("close_account", crate::MdEventFamily::TokenAccount, false), - ); - assert_eq!(parent.outputs[0].payload_json["lifecycleKind"], "nested_ata_recovered"); - assert_eq!( - parent.outputs[0].payload_json["ownership"]["ataParent"], - "associated_address_lifecycle_only" - ); - assert!(parent.outputs[0].payload_json.get("amountRaw").is_none()); - assert_eq!(transfer.outputs[0].payload_json["operation"], "transfer"); - assert_eq!(close.outputs[0].payload_json["operation"], "close_account"); - assert_eq!(transfer.outputs[0].payload_json["domain"], "spl_token_account_mutation"); - assert_eq!(close.outputs[0].payload_json["domain"], "spl_token_account_mutation"); - } - - #[test] - fn ata_matrix_and_compiled_token_account_projection_ownership_are_equal() { - let matrix: serde_json::Value = match serde_json::from_str(include_str!( - "../../../../test-fixtures/contract-matrices/SPL_ASSOCIATED_TOKEN_ACCOUNT_MATRIX.json" - )) { - std::result::Result::Ok(value) => value, - std::result::Result::Err(error) => panic!("ATA matrix is invalid: {error}"), - }; - let instructions = match matrix.get("instructions").and_then(serde_json::Value::as_array) { - std::option::Option::Some(value) => value, - std::option::Option::None => panic!("ATA matrix instructions are absent"), - }; - for (entry, fact) in super::ATA_LIFECYCLE_ENTRIES { - let row = match instructions.iter().find(|row| { - return row.get("name").and_then(serde_json::Value::as_str) - == std::option::Option::Some(*entry); - }) { - std::option::Option::Some(value) => value, - std::option::Option::None => panic!("ATA matrix entry {entry} is absent"), - }; - let expected = format!("token_accounts:{fact}"); - assert!(row["authorizedProjections"].as_array().is_some_and(|values| { - return values.iter().any(|value| return value == expected.as_str()); - })); - } - let owner = &matrix["materializationContract"]["projectionOwners"][0]; - let matrix_facts = match owner["facts"].as_array() { - std::option::Option::Some(values) => values - .iter() - .filter_map(|value| return value.as_str()) - .collect::>(), - std::option::Option::None => panic!("ATA token-account owner facts are absent"), - }; - let compiled_facts = super::ATA_LIFECYCLE_ENTRIES - .iter() - .map(|(_, fact)| return *fact) - .collect::>(); - assert_eq!(matrix_facts, compiled_facts); - } - - #[test] - fn second_ata_materialization_is_byte_for_byte_idempotent() { - let observation = ata_observation("create_idempotent", false, true); - let first = crate::MtApiEventMaterializer::materialize( - &crate::MtTokenAccountsMaterializer, - &observation, - ); - let second = crate::MtApiEventMaterializer::materialize( - &crate::MtTokenAccountsMaterializer, - &observation, - ); - assert_eq!(first, second); - assert_eq!(first.outputs[0].output_key, second.outputs[0].output_key); - assert_eq!( - first.outputs[0].payload_json["idempotenceKey"], - second.outputs[0].payload_json["idempotenceKey"] - ); - } - - #[test] - fn token_2022_state_snapshot_preserves_base_and_ordered_extensions() { - let state = crate::DcToken2022State { - kind: crate::DcToken2022StateKind::Mint, - base_fields: serde_json::json!({"supply":"42","decimals":9,"initialized":true}), - base_hex: "00".repeat(82), - account_type: std::option::Option::Some(1), - extensions: std::vec![ - crate::DcToken2022TlvEntry { - extension_type: 1, - extension_name: "transfer_fee_config", - value_hex: "11".repeat(108), - value_fields: serde_json::json!({"withheldAmount":"7"}), - }, - crate::DcToken2022TlvEntry { - extension_type: 4, - extension_name: "confidential_transfer_mint", - value_hex: "22".repeat(65), - value_fields: serde_json::json!({"autoApproveNewAccounts":true}), - }, - ], - }; - let output = match crate::materializer_token_materialize_token_2022_state_snapshot( - "mint111", 99, &state, - ) { - std::result::Result::Ok(value) => value, - std::result::Result::Err(error) => panic!("snapshot failed: {error}"), - }; - assert_eq!(output.payload_json["stateKind"], "mint"); - assert_eq!(output.payload_json["extensionCount"], 2); - assert_eq!(output.payload_json["extensionNames"][0], "transfer_fee_config"); - assert_eq!(output.payload_json["extensionNames"][1], "confidential_transfer_mint"); - assert_eq!(output.payload_json["containsConfidentialState"], true); - assert_eq!(output.payload_json["confidentialValuesDecrypted"], false); - assert_eq!(output.payload_json["baseFields"]["supply"], "42"); - } - - #[test] - fn token_2022_state_snapshot_is_idempotent_and_rejects_missing_identity() { - let state = crate::DcToken2022State { - kind: crate::DcToken2022StateKind::Account, - base_fields: serde_json::json!({"mint":"mint111","owner":"wallet111","amount":"5"}), - base_hex: "00".repeat(165), - account_type: std::option::Option::None, - extensions: std::vec::Vec::new(), - }; - let first = crate::materializer_token_materialize_token_2022_state_snapshot( - "account111", - 7, - &state, - ); - let second = crate::materializer_token_materialize_token_2022_state_snapshot( - "account111", - 7, - &state, - ); - assert_eq!(first, second); - assert!( - crate::materializer_token_materialize_token_2022_state_snapshot("", 7, &state).is_err() - ); - } -} +/// Crate-visible `MT_TOKEN_ACCOUNTS_ACCEPTED_FAMILIES` component constant. +pub(crate) use self::constants::MT_TOKEN_ACCOUNTS_ACCEPTED_FAMILIES; +/// Crate-visible `MT_TOKEN_ACCOUNTS_ATA_LIFECYCLE_ENTRIES` component constant. +pub(crate) use self::constants::MT_TOKEN_ACCOUNTS_ATA_LIFECYCLE_ENTRIES; +/// Stable runtime component name for Token, ATA and Token-2022 account projections. +pub(crate) use self::constants::MT_TOKEN_ACCOUNTS_COMPONENT_NAME; +/// Crate-visible `MT_TOKEN_ACCOUNTS_MUTATION_ENTRIES` component constant. +pub(crate) use self::constants::MT_TOKEN_ACCOUNTS_MUTATION_ENTRIES; +/// Stable processor name for Token, ATA and Token-2022 account projections. +pub(crate) use self::constants::MT_TOKEN_ACCOUNTS_PROCESSOR_NAME; +/// Stable projection contract version for Token, ATA and Token-2022 account projections. +pub(crate) use self::constants::MT_TOKEN_ACCOUNTS_PROJECTION_VERSION; +/// Canonical tracing target for Token, ATA and Token-2022 account projections. +pub(crate) use self::constants::TRACING_TARGET_MATERIALIZER_TOKEN_ACCOUNTS; diff --git a/kb-lib/src/materializer/token/accounts/constants.rs b/kb-lib/src/materializer/token/accounts/constants.rs new file mode 100644 index 0000000..a7f7f6c --- /dev/null +++ b/kb-lib/src/materializer/token/accounts/constants.rs @@ -0,0 +1,54 @@ +// file: kb-lib/src/materializer/token/accounts/constants.rs +// version: 4 + +//! Local constants for the token accounts materializer. + +/// Stable runtime component name for the token accounts materializer. +pub(crate) const MT_TOKEN_ACCOUNTS_COMPONENT_NAME: &str = "kb-lib.materializer.token.accounts"; +/// Stable processor name persisted by the replay ledger. +pub(crate) const MT_TOKEN_ACCOUNTS_PROCESSOR_NAME: &str = "materializer.token.accounts"; +/// Stable projection contract version for the token accounts materializer. +pub(crate) const MT_TOKEN_ACCOUNTS_PROJECTION_VERSION: u32 = 3; +/// Canonical tracing target for the token accounts materializer. +pub(crate) const TRACING_TARGET_MATERIALIZER_TOKEN_ACCOUNTS: &str = + "kb-lib.materializer.token.accounts"; + +/// Component contract for `accepted_families`. +pub(crate) const MT_TOKEN_ACCOUNTS_ACCEPTED_FAMILIES: &[crate::MdEventFamily] = &[ + crate::MdEventFamily::TokenAccount, + crate::MdEventFamily::TokenMint, + crate::MdEventFamily::TokenBurn, + crate::MdEventFamily::Lifecycle, +]; +/// Component contract for `mutation_entries`. +pub(crate) const MT_TOKEN_ACCOUNTS_MUTATION_ENTRIES: &[&str] = &[ + "initialize_mint", + "initialize_mint2", + "initialize_account", + "initialize_account2", + "initialize_account3", + "transfer", + "transfer_checked", + "approve", + "approve_checked", + "revoke", + "mint_to", + "mint_to_checked", + "burn", + "burn_checked", + "close_account", + "freeze_account", + "thaw_account", + "sync_native", + "initialize_immutable_owner", + "withdraw_excess_lamports", + "unwrap_lamports", + "initialize_non_transferable_mint", + "reallocate", +]; +/// Component contract for `ata_lifecycle_entries`. +pub(crate) const MT_TOKEN_ACCOUNTS_ATA_LIFECYCLE_ENTRIES: &[(&str, &str)] = &[ + ("create", "ata_created"), + ("create_idempotent", "ata_created_or_reused_idempotently"), + ("recover_nested", "nested_ata_recovered"), +]; diff --git a/kb-lib/src/materializer/token/accounts/materializer.rs b/kb-lib/src/materializer/token/accounts/materializer.rs new file mode 100644 index 0000000..6deec46 --- /dev/null +++ b/kb-lib/src/materializer/token/accounts/materializer.rs @@ -0,0 +1,809 @@ +// file: kb-lib/src/materializer/token/accounts/materializer.rs +// version: 9 + +//! Stable committed SPL Token mutation and ATA lifecycle projections. + +/// Stable committed classic SPL Token account and mint mutation materializer. +#[derive(Clone, Debug, Default)] +pub struct MtTokenAccountsMaterializer; + +impl crate::MtMaterializer for crate::MtTokenAccountsMaterializer { + fn materializer_name(&self) -> &'static str { + return crate::MT_TOKEN_ACCOUNTS_COMPONENT_NAME; + } + + fn materializer_version(&self) -> &'static str { + return env!("CARGO_PKG_VERSION"); + } + + fn accepts_event(&self, event: &crate::MdDecodedProtocolEvent) -> bool { + return accepts_event(event); + } + + fn materialize_event( + &self, + _event: &crate::MdDecodedProtocolEvent, + ) -> kb_core::Result> { + return std::result::Result::Ok(std::vec::Vec::new()); + } +} + +impl crate::MtApiEventMaterializer for crate::MtTokenAccountsMaterializer { + fn identity(&self) -> crate::MtApiMaterializerIdentity { + return crate::MtApiMaterializerIdentity { + name: crate::MT_TOKEN_ACCOUNTS_PROCESSOR_NAME.to_string(), + version: env!("CARGO_PKG_VERSION").to_string(), + }; + } + + fn accepted_families(&self) -> &'static [crate::MdEventFamily] { + return crate::MT_TOKEN_ACCOUNTS_ACCEPTED_FAMILIES; + } + + fn accepts_observation(&self, observation: &crate::DcApiDecodedObservation) -> bool { + return accepts_event(&observation.event); + } + + fn transaction_policy( + &self, + _family: crate::MdEventFamily, + ) -> crate::MtApiMaterializationTransactionPolicy { + return crate::MtApiMaterializationTransactionPolicy::SuccessfulCommittedOnly; + } + + fn materialize( + &self, + observation: &crate::DcApiDecodedObservation, + ) -> crate::MtApiMaterializerExecutionResult { + if !accepts_event(&observation.event) { + return crate::MtApiMaterializerExecutionResult::ignored(); + } + if observation.transaction_failed || !observation.observation_committed { + return crate::MtApiMaterializerExecutionResult::refused( + "uncommitted_token_account_projection_refused", + "failed or uncommitted SPL Token or ATA observations cannot create mutable outputs", + ); + } + if is_ata_event(&observation.event) { + return materialize_ata(observation); + } + let parameters = observation + .payload_json + .get("parameters") + .cloned() + .unwrap_or(serde_json::Value::Null); + let accounts = observation + .payload_json + .get("accounts") + .cloned() + .unwrap_or(serde_json::Value::Null); + let authority = observation + .payload_json + .get("authority") + .cloned() + .unwrap_or(serde_json::Value::Null); + let operation = observation.event.event_name.0.clone(); + let output = crate::MtApiMaterializedOutput { + output_key: format!("token_account_mutation:{operation}:0"), + family: crate::MdMaterializedEventFamily::TokenAccount, + payload_json: serde_json::json!({ + "projectionVersion": crate::MT_TOKEN_ACCOUNTS_PROJECTION_VERSION, + "domain": "spl_token_account_mutation", + "projectionSemantics": "committed_instruction_mutation_not_final_account_snapshot", + "idempotenceKey": format!( + "spl_token:{}:{}:{}", + observation.event.signature.0, + observation.event.instruction_path.0, + operation + ), + "operation": operation, + "signature": observation.event.signature.0.clone(), + "slot": observation.event.slot.0, + "instructionPath": observation.event.instruction_path.0.clone(), + "programId": observation.event.program_id.0.clone(), + "mint": account_key_for_role(&accounts, "mint"), + "amountRaw": parameters.get("amountRaw").cloned().unwrap_or(serde_json::Value::Null), + "decimals": parameters.get("decimals").cloned().unwrap_or(serde_json::Value::Null), + "parameters": parameters, + "accounts": accounts, + "authority": authority, + "committed": true, + "transactionFinalAccountStateCaptured": false, + "coreBalanceChangesDuplicated": false, + "provenance": { + "processorName": crate::MT_TOKEN_ACCOUNTS_PROCESSOR_NAME, + "processorVersion": env!("CARGO_PKG_VERSION"), + "sourceSurface": observation.event.surface_code.0.clone(), + "sourceEventCode": observation.event.event_code.0.clone(), + "sourceEventKey": observation.event_key.clone() + } + }), + }; + tracing::debug!( + target: crate::TRACING_TARGET_MATERIALIZER_TOKEN_ACCOUNTS, + action = "materialize_token_account_mutation", + signature = %observation.event.signature.0, + instruction_path = %observation.event.instruction_path.0, + operation = %observation.event.event_name.0, + "materialize committed classic SPL Token mutation" + ); + return crate::MtApiMaterializerExecutionResult { + status: crate::MtApiMaterializerOutcomeStatus::Inserted, + outputs: std::vec![output], + diagnostics: std::vec::Vec::new(), + }; + } +} + +fn accepts_event(event: &crate::MdDecodedProtocolEvent) -> bool { + let token_program = (event.program_id.0 == kb_program_ids::SPL_TOKEN_PROGRAM_ID + && event.surface_code.0 == "spl.token") + || (event.program_id.0 == kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID + && event.surface_code.0 == "spl.token_2022"); + let token_mutation = token_program + && crate::MT_TOKEN_ACCOUNTS_MUTATION_ENTRIES.contains(&event.event_name.0.as_str()); + let ata = is_ata_event(event); + return crate::MT_TOKEN_ACCOUNTS_ACCEPTED_FAMILIES.contains(&event.event_family) + && (token_mutation || ata); +} + +fn is_ata_event(event: &crate::MdDecodedProtocolEvent) -> bool { + return event.program_id.0 == kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID + && event.surface_code.0 == "spl.associated_token_account" + && event.event_family == crate::MdEventFamily::Lifecycle + && crate::MT_TOKEN_ACCOUNTS_ATA_LIFECYCLE_ENTRIES + .iter() + .any(|(entry, _)| return *entry == event.event_name.0); +} + +fn materialize_ata( + observation: &crate::DcApiDecodedObservation, +) -> crate::MtApiMaterializerExecutionResult { + let operation = observation.event.event_name.0.as_str(); + let addresses = match observation.payload_json.get("addresses") { + std::option::Option::Some(value) if value.is_object() => value, + _ => { + return crate::MtApiMaterializerExecutionResult::refused( + "ata_addresses_missing", + "committed ATA lifecycle projection requires the decoded address contract", + ); + }, + }; + let validation = validate_ata_addresses(operation, addresses); + if let std::result::Result::Err((code, message)) = validation { + return crate::MtApiMaterializerExecutionResult::refused(code, message); + } + let lifecycle_kind = match crate::MT_TOKEN_ACCOUNTS_ATA_LIFECYCLE_ENTRIES + .iter() + .find(|(entry, _)| return *entry == operation) + { + std::option::Option::Some((_, value)) => *value, + std::option::Option::None => { + return crate::MtApiMaterializerExecutionResult::ignored(); + }, + }; + let associated_token_account = if operation == "recover_nested" { + observed_address(addresses, "nestedAssociatedTokenAccount") + } else { + observed_address(addresses, "associatedTokenAccount") + }; + let token_program = addresses + .get("tokenProgram") + .and_then(|value| return value.get("programId")) + .cloned() + .unwrap_or(serde_json::Value::Null); + let wallet = addresses.get("walletOwner").cloned().unwrap_or(serde_json::Value::Null); + let mint = if operation == "recover_nested" { + addresses.get("nestedMint").cloned().unwrap_or(serde_json::Value::Null) + } else { + addresses.get("mint").cloned().unwrap_or(serde_json::Value::Null) + }; + let output = crate::MtApiMaterializedOutput { + output_key: format!("ata_lifecycle:{lifecycle_kind}:0"), + family: crate::MdMaterializedEventFamily::TokenAccount, + payload_json: serde_json::json!({ + "projectionVersion": crate::MT_TOKEN_ACCOUNTS_PROJECTION_VERSION, + "domain": "spl_associated_token_account_lifecycle", + "projectionSemantics": "committed_associated_address_lifecycle_not_token_state_snapshot", + "idempotenceKey": format!( + "ata:{}:{}:{}:{}:{}", + observation.event.signature.0, + observation.event.instruction_path.0, + observation.event.program_id.0, + lifecycle_kind, + associated_token_account.as_str().unwrap_or("") + ), + "operation": operation, + "lifecycleKind": lifecycle_kind, + "associatedTokenAccount": associated_token_account, + "walletOwner": wallet, + "mint": mint, + "tokenProgramId": token_program, + "fundingAccount": addresses.get("fundingAccount").cloned().unwrap_or(serde_json::Value::Null), + "ownerMint": addresses.get("ownerMint").cloned().unwrap_or(serde_json::Value::Null), + "nestedMint": addresses.get("nestedMint").cloned().unwrap_or(serde_json::Value::Null), + "ownerAssociatedTokenAccount": observed_address(addresses, "ownerAssociatedTokenAccount"), + "nestedAssociatedTokenAccount": observed_address(addresses, "nestedAssociatedTokenAccount"), + "walletNestedMintAssociatedTokenAccount": observed_address(addresses, "walletNestedMintAssociatedTokenAccount"), + "idempotentOutcome": if operation == "create_idempotent" { + "created_or_reused_not_distinguishable_from_parent_instruction_alone" + } else { + "not_applicable" + }, + "signature": observation.event.signature.0.clone(), + "slot": observation.event.slot.0, + "instructionPath": observation.event.instruction_path.0.clone(), + "programId": observation.event.program_id.0.clone(), + "committed": true, + "transactionFinalAccountStateCaptured": false, + "tokenBalanceReconstructed": false, + "splTokenCpiMutationsDuplicated": false, + "ownership": { + "ataParent": "associated_address_lifecycle_only", + "splTokenCpi": "initialize_transfer_and_close_mutations" + }, + "provenance": { + "processorName": crate::MT_TOKEN_ACCOUNTS_PROCESSOR_NAME, + "processorVersion": env!("CARGO_PKG_VERSION"), + "sourceSurface": observation.event.surface_code.0.clone(), + "sourceEventCode": observation.event.event_code.0.clone(), + "sourceEventKey": observation.event_key.clone() + } + }), + }; + tracing::debug!( + target: crate::TRACING_TARGET_MATERIALIZER_TOKEN_ACCOUNTS, + action = "materialize_ata_lifecycle", + signature = %observation.event.signature.0, + instruction_path = %observation.event.instruction_path.0, + operation, + lifecycle_kind, + "materialize committed ATA lifecycle without duplicating SPL Token CPI mutations" + ); + return crate::MtApiMaterializerExecutionResult { + status: crate::MtApiMaterializerOutcomeStatus::Inserted, + outputs: std::vec![output], + diagnostics: std::vec::Vec::new(), + }; +} + +fn validate_ata_addresses( + operation: &str, + addresses: &serde_json::Value, +) -> std::result::Result<(), (&'static str, &'static str)> { + let token_supported = addresses + .get("tokenProgram") + .and_then(|value| return value.get("supported")) + .and_then(serde_json::Value::as_bool) + == std::option::Option::Some(true); + let token_program_id = addresses + .get("tokenProgram") + .and_then(|value| return value.get("programId")) + .and_then(serde_json::Value::as_str); + let token_program_exact = matches!( + token_program_id, + std::option::Option::Some(kb_program_ids::SPL_TOKEN_PROGRAM_ID) + | std::option::Option::Some(kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID) + ); + if !token_supported || !token_program_exact { + return std::result::Result::Err(( + "ata_token_program_not_supported", + "ATA lifecycle projection requires classic SPL Token or Token-2022", + )); + } + let fields: &[&str] = if operation == "recover_nested" { + &[ + "ownerAssociatedTokenAccount", + "nestedAssociatedTokenAccount", + "walletNestedMintAssociatedTokenAccount", + ] + } else { + &["associatedTokenAccount"] + }; + for field in fields { + let valid = addresses + .get(*field) + .and_then(|value| return value.get("valid")) + .and_then(serde_json::Value::as_bool) + == std::option::Option::Some(true); + let observed = observed_address(addresses, field); + if !valid || observed.as_str().is_none_or(|value| return value.is_empty()) { + return std::result::Result::Err(( + "ata_derived_address_invalid", + "ATA lifecycle projection requires every canonical address validation to pass", + )); + } + } + let identity_fields: &[&str] = if operation == "recover_nested" { + &["walletOwner", "ownerMint", "nestedMint"] + } else { + &["walletOwner", "mint"] + }; + if identity_fields.iter().any(|field| { + return addresses + .get(*field) + .and_then(serde_json::Value::as_str) + .is_none_or(|value| return value.is_empty()); + }) { + return std::result::Result::Err(( + "ata_identity_field_missing", + "ATA lifecycle projection requires wallet and mint identities", + )); + } + return std::result::Result::Ok(()); +} + +fn observed_address(addresses: &serde_json::Value, field: &str) -> serde_json::Value { + return addresses + .get(field) + .and_then(|value| return value.get("observed")) + .cloned() + .unwrap_or(serde_json::Value::Null); +} + +fn account_key_for_role(accounts: &serde_json::Value, role: &str) -> serde_json::Value { + let value = accounts.as_array().and_then(|rows| { + return rows.iter().find_map(|row| { + if row.get("role").and_then(serde_json::Value::as_str) + != std::option::Option::Some(role) + { + return std::option::Option::None; + } + return row.get("accountKey").and_then(serde_json::Value::as_str); + }); + }); + return match value { + std::option::Option::Some(account_key) => { + serde_json::Value::String(account_key.to_string()) + }, + std::option::Option::None => serde_json::Value::Null, + }; +} + +/// Materialize one already parsed Token-2022 account snapshot without inferring hidden state. +pub fn materializer_token_materialize_token_2022_state_snapshot( + account_key: &str, + slot: u64, + state: &crate::DcToken2022State, +) -> std::result::Result { + if account_key.is_empty() { + return std::result::Result::Err( + "Token-2022 state snapshot requires a non-empty account key".to_string(), + ); + } + let kind = match state.kind { + crate::DcToken2022StateKind::Mint => "mint", + crate::DcToken2022StateKind::Account => "account", + crate::DcToken2022StateKind::Multisig => "multisig", + }; + let extensions = state + .extensions + .iter() + .map(|entry| { + return serde_json::json!({ + "extensionType": entry.extension_type, + "extensionName": entry.extension_name, + "valueHex": entry.value_hex, + "valueFields": entry.value_fields, + }); + }) + .collect::>(); + let extension_names = state + .extensions + .iter() + .map(|entry| return entry.extension_name) + .collect::>(); + let contains_confidential_state = state.extensions.iter().any(|entry| { + return entry.extension_name.starts_with("confidential_") + || entry.extension_name == "confidential_mint_burn"; + }); + return std::result::Result::Ok(crate::MtApiMaterializedOutput { + output_key: format!("token_2022_state_snapshot:{account_key}:{slot}"), + family: crate::MdMaterializedEventFamily::TokenAccount, + payload_json: serde_json::json!({ + "projectionVersion": crate::MT_TOKEN_ACCOUNTS_PROJECTION_VERSION, + "domain": "spl_token_2022_account_state", + "projectionSemantics": "authoritative_bounded_account_snapshot", + "idempotenceKey": format!("token_2022_state:{account_key}:{slot}"), + "accountKey": account_key, + "slot": slot, + "programId": kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID, + "stateKind": kind, + "baseFields": state.base_fields, + "baseHex": state.base_hex, + "accountType": state.account_type, + "extensionCount": state.extensions.len(), + "extensionNames": extension_names, + "extensions": extensions, + "containsConfidentialState": contains_confidential_state, + "confidentialValuesDecrypted": false, + "finalAccountStateCaptured": true, + "provenance": { + "processorName": crate::MT_TOKEN_ACCOUNTS_PROCESSOR_NAME, + "processorVersion": env!("CARGO_PKG_VERSION"), + "source": "bounded_token_2022_state_parser" + } + }), + }); +} + +#[cfg(test)] +mod tests { + fn observation( + entry: &str, + family: crate::MdEventFamily, + failed: bool, + ) -> crate::DcApiDecodedObservation { + return crate::DcApiDecodedObservation { + event_key: format!("token:{entry}:0"), + event: crate::MdDecodedProtocolEvent { + signature: crate::MdSignature("signature".to_string()), + slot: crate::MdSlot(42), + instruction_path: crate::MdInstructionPath("1/0".to_string()), + program_id: crate::MdProgramId(kb_program_ids::SPL_TOKEN_PROGRAM_ID.to_string()), + protocol_code: crate::MdProtocolCode("spl.token".to_string()), + surface_code: crate::MdSurfaceCode("spl.token".to_string()), + event_code: crate::MdEventCode(format!("spl.token.{entry}")), + event_name: crate::MdEventName(entry.to_string()), + event_family: family, + source_kind: crate::MdEventSourceKind::InnerInstruction, + confidence: crate::MdDecoderConfidence::ManualExact, + }, + payload_json: serde_json::json!({ + "parameters": {"amountRaw":"18446744073709551615","decimals":9}, + "accounts": [{"role":"mint","accountKey":"mint111"}], + "authority": {"form":"single"} + }), + transaction_failed: failed, + transaction_error: if failed { + std::option::Option::Some(serde_json::json!({"error":"failed"})) + } else { + std::option::Option::None + }, + observation_committed: !failed, + proof: crate::DcApiDecoderProof { + kind: crate::DcApiDecoderProofKind::Manual, + confidence: crate::MdDecoderConfidence::ManualExact, + evidence: std::vec!["fixture".to_string()], + }, + }; + } + + fn token_2022_observation( + entry: &str, + family: crate::MdEventFamily, + failed: bool, + ) -> crate::DcApiDecodedObservation { + let mut observation = observation(entry, family, failed); + observation.event.program_id = + crate::MdProgramId(kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID.to_string()); + observation.event.protocol_code = crate::MdProtocolCode("spl.token_2022".to_string()); + observation.event.surface_code = crate::MdSurfaceCode("spl.token_2022".to_string()); + observation.event.event_code = crate::MdEventCode(format!("spl.token_2022.{entry}")); + observation.event_key = format!("token_2022:{entry}:0"); + return observation; + } + + fn ata_observation(entry: &str, failed: bool, valid: bool) -> crate::DcApiDecodedObservation { + let addresses = if entry == "recover_nested" { + serde_json::json!({ + "tokenProgram": { + "programId": kb_program_ids::SPL_TOKEN_PROGRAM_ID, + "kind": "spl_token_classic", + "supported": true + }, + "ownerAssociatedTokenAccount": {"expected":"ownerAta","observed":"ownerAta","valid":valid}, + "nestedAssociatedTokenAccount": {"expected":"nestedAta","observed":"nestedAta","valid":valid}, + "walletNestedMintAssociatedTokenAccount": {"expected":"destinationAta","observed":"destinationAta","valid":valid}, + "walletOwner": "wallet111", + "ownerMint": "ownerMint111", + "nestedMint": "nestedMint111" + }) + } else { + serde_json::json!({ + "tokenProgram": { + "programId": kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID, + "kind": "token_2022", + "supported": true + }, + "associatedTokenAccount": {"expected":"ata111","observed":"ata111","valid":valid}, + "fundingAccount": "payer111", + "walletOwner": "wallet111", + "mint": "mint111" + }) + }; + return crate::DcApiDecodedObservation { + event_key: format!("ata:{entry}:0"), + event: crate::MdDecodedProtocolEvent { + signature: crate::MdSignature("signature".to_string()), + slot: crate::MdSlot(42), + instruction_path: crate::MdInstructionPath("0".to_string()), + program_id: crate::MdProgramId( + kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID.to_string(), + ), + protocol_code: crate::MdProtocolCode("spl.associated_token_account".to_string()), + surface_code: crate::MdSurfaceCode("spl.associated_token_account".to_string()), + event_code: crate::MdEventCode(format!("spl.associated_token_account.{entry}")), + event_name: crate::MdEventName(entry.to_string()), + event_family: crate::MdEventFamily::Lifecycle, + source_kind: crate::MdEventSourceKind::Instruction, + confidence: crate::MdDecoderConfidence::ManualExact, + }, + payload_json: serde_json::json!({"addresses":addresses}), + transaction_failed: failed, + transaction_error: std::option::Option::None, + observation_committed: !failed, + proof: crate::DcApiDecoderProof { + kind: crate::DcApiDecoderProofKind::Manual, + confidence: crate::MdDecoderConfidence::ManualExact, + evidence: std::vec!["fixture".to_string()], + }, + }; + } + + #[test] + fn committed_checked_transfer_preserves_exact_amount_mint_and_path() { + let observation = + observation("transfer_checked", crate::MdEventFamily::TokenAccount, false); + let result = crate::MtApiEventMaterializer::materialize( + &crate::MtTokenAccountsMaterializer, + &observation, + ); + assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted); + assert_eq!(result.outputs[0].payload_json["amountRaw"], "18446744073709551615"); + assert_eq!(result.outputs[0].payload_json["mint"], "mint111"); + assert_eq!(result.outputs[0].payload_json["instructionPath"], "1/0"); + assert_eq!(result.outputs[0].payload_json["transactionFinalAccountStateCaptured"], false); + } + + #[test] + fn committed_token_2022_transfer_has_the_same_single_projection_owner() { + let observation = + token_2022_observation("transfer_checked", crate::MdEventFamily::TokenAccount, false); + let result = crate::MtApiEventMaterializer::materialize( + &crate::MtTokenAccountsMaterializer, + &observation, + ); + assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted); + assert_eq!(result.outputs.len(), 1); + assert_eq!(result.outputs[0].payload_json["operation"], "transfer_checked"); + assert_eq!( + result.outputs[0].payload_json["programId"], + kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID + ); + } + + #[test] + fn failed_mutation_is_refused() { + let observation = observation("burn", crate::MdEventFamily::TokenBurn, true); + let result = crate::MtApiEventMaterializer::materialize( + &crate::MtTokenAccountsMaterializer, + &observation, + ); + assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Refused); + assert!(result.outputs.is_empty()); + } + + #[test] + fn batch_parent_admin_and_conversion_events_are_not_duplicated() { + for (entry, family) in [ + ("batch", crate::MdEventFamily::Audit), + ("set_authority", crate::MdEventFamily::Admin), + ("amount_to_ui_amount", crate::MdEventFamily::Audit), + ] { + let observation = observation(entry, family, false); + let result = crate::MtApiEventMaterializer::materialize( + &crate::MtTokenAccountsMaterializer, + &observation, + ); + assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Ignored); + } + } + + #[test] + fn every_ata_variant_has_one_exact_lifecycle_projection() { + for (entry, expected_kind, expected_ata, expected_token_program) in [ + ("create", "ata_created", "ata111", kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID), + ( + "create_idempotent", + "ata_created_or_reused_idempotently", + "ata111", + kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID, + ), + ( + "recover_nested", + "nested_ata_recovered", + "nestedAta", + kb_program_ids::SPL_TOKEN_PROGRAM_ID, + ), + ] { + let result = crate::MtApiEventMaterializer::materialize( + &crate::MtTokenAccountsMaterializer, + &ata_observation(entry, false, true), + ); + assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted); + assert_eq!(result.outputs.len(), 1); + assert_eq!(result.outputs[0].payload_json["lifecycleKind"], expected_kind); + assert_eq!(result.outputs[0].payload_json["associatedTokenAccount"], expected_ata); + assert_eq!(result.outputs[0].payload_json["tokenProgramId"], expected_token_program); + assert_eq!(result.outputs[0].payload_json["tokenBalanceReconstructed"], false); + assert_eq!(result.outputs[0].payload_json["splTokenCpiMutationsDuplicated"], false); + } + } + + #[test] + fn idempotent_parent_does_not_invent_created_or_reused_branch() { + let result = crate::MtApiEventMaterializer::materialize( + &crate::MtTokenAccountsMaterializer, + &ata_observation("create_idempotent", false, true), + ); + assert_eq!( + result.outputs[0].payload_json["idempotentOutcome"], + "created_or_reused_not_distinguishable_from_parent_instruction_alone" + ); + assert!(result.outputs[0].payload_json.get("reused").is_none()); + assert!(result.outputs[0].payload_json.get("created").is_none()); + } + + #[test] + fn uncommitted_or_invalid_ata_lifecycle_is_refused() { + for observation in [ + ata_observation("create", true, true), + ata_observation("recover_nested", false, false), + ] { + let result = crate::MtApiEventMaterializer::materialize( + &crate::MtTokenAccountsMaterializer, + &observation, + ); + assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Refused); + assert!(result.outputs.is_empty()); + } + } + + #[test] + fn ata_parent_and_classic_token_cpi_children_own_distinct_facts() { + let parent = crate::MtApiEventMaterializer::materialize( + &crate::MtTokenAccountsMaterializer, + &ata_observation("recover_nested", false, true), + ); + let transfer = crate::MtApiEventMaterializer::materialize( + &crate::MtTokenAccountsMaterializer, + &observation("transfer", crate::MdEventFamily::TokenAccount, false), + ); + let close = crate::MtApiEventMaterializer::materialize( + &crate::MtTokenAccountsMaterializer, + &observation("close_account", crate::MdEventFamily::TokenAccount, false), + ); + assert_eq!(parent.outputs[0].payload_json["lifecycleKind"], "nested_ata_recovered"); + assert_eq!( + parent.outputs[0].payload_json["ownership"]["ataParent"], + "associated_address_lifecycle_only" + ); + assert!(parent.outputs[0].payload_json.get("amountRaw").is_none()); + assert_eq!(transfer.outputs[0].payload_json["operation"], "transfer"); + assert_eq!(close.outputs[0].payload_json["operation"], "close_account"); + assert_eq!(transfer.outputs[0].payload_json["domain"], "spl_token_account_mutation"); + assert_eq!(close.outputs[0].payload_json["domain"], "spl_token_account_mutation"); + } + + #[test] + fn ata_matrix_and_compiled_token_account_projection_ownership_are_equal() { + let matrix: serde_json::Value = match serde_json::from_str(include_str!( + "../../../../../test-fixtures/contract-matrices/SPL_ASSOCIATED_TOKEN_ACCOUNT_MATRIX.json" + )) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("ATA matrix is invalid: {error}"), + }; + let instructions = match matrix.get("instructions").and_then(serde_json::Value::as_array) { + std::option::Option::Some(value) => value, + std::option::Option::None => panic!("ATA matrix instructions are absent"), + }; + for (entry, fact) in crate::MT_TOKEN_ACCOUNTS_ATA_LIFECYCLE_ENTRIES { + let row = match instructions.iter().find(|row| { + return row.get("name").and_then(serde_json::Value::as_str) + == std::option::Option::Some(*entry); + }) { + std::option::Option::Some(value) => value, + std::option::Option::None => panic!("ATA matrix entry {entry} is absent"), + }; + let expected = format!("token_accounts:{fact}"); + assert!(row["authorizedProjections"].as_array().is_some_and(|values| { + return values.iter().any(|value| return value == expected.as_str()); + })); + } + let owner = &matrix["materializationContract"]["projectionOwners"][0]; + let matrix_facts = match owner["facts"].as_array() { + std::option::Option::Some(values) => values + .iter() + .filter_map(|value| return value.as_str()) + .collect::>(), + std::option::Option::None => panic!("ATA token-account owner facts are absent"), + }; + let compiled_facts = crate::MT_TOKEN_ACCOUNTS_ATA_LIFECYCLE_ENTRIES + .iter() + .map(|(_, fact)| return *fact) + .collect::>(); + assert_eq!(matrix_facts, compiled_facts); + } + + #[test] + fn second_ata_materialization_is_byte_for_byte_idempotent() { + let observation = ata_observation("create_idempotent", false, true); + let first = crate::MtApiEventMaterializer::materialize( + &crate::MtTokenAccountsMaterializer, + &observation, + ); + let second = crate::MtApiEventMaterializer::materialize( + &crate::MtTokenAccountsMaterializer, + &observation, + ); + assert_eq!(first, second); + assert_eq!(first.outputs[0].output_key, second.outputs[0].output_key); + assert_eq!( + first.outputs[0].payload_json["idempotenceKey"], + second.outputs[0].payload_json["idempotenceKey"] + ); + } + + #[test] + fn token_2022_state_snapshot_preserves_base_and_ordered_extensions() { + let state = crate::DcToken2022State { + kind: crate::DcToken2022StateKind::Mint, + base_fields: serde_json::json!({"supply":"42","decimals":9,"initialized":true}), + base_hex: "00".repeat(82), + account_type: std::option::Option::Some(1), + extensions: std::vec![ + crate::DcToken2022TlvEntry { + extension_type: 1, + extension_name: "transfer_fee_config", + value_hex: "11".repeat(108), + value_fields: serde_json::json!({"withheldAmount":"7"}), + }, + crate::DcToken2022TlvEntry { + extension_type: 4, + extension_name: "confidential_transfer_mint", + value_hex: "22".repeat(65), + value_fields: serde_json::json!({"autoApproveNewAccounts":true}), + }, + ], + }; + let output = match crate::materializer_token_materialize_token_2022_state_snapshot( + "mint111", 99, &state, + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("snapshot failed: {error}"), + }; + assert_eq!(output.payload_json["stateKind"], "mint"); + assert_eq!(output.payload_json["extensionCount"], 2); + assert_eq!(output.payload_json["extensionNames"][0], "transfer_fee_config"); + assert_eq!(output.payload_json["extensionNames"][1], "confidential_transfer_mint"); + assert_eq!(output.payload_json["containsConfidentialState"], true); + assert_eq!(output.payload_json["confidentialValuesDecrypted"], false); + assert_eq!(output.payload_json["baseFields"]["supply"], "42"); + } + + #[test] + fn token_2022_state_snapshot_is_idempotent_and_rejects_missing_identity() { + let state = crate::DcToken2022State { + kind: crate::DcToken2022StateKind::Account, + base_fields: serde_json::json!({"mint":"mint111","owner":"wallet111","amount":"5"}), + base_hex: "00".repeat(165), + account_type: std::option::Option::None, + extensions: std::vec::Vec::new(), + }; + let first = crate::materializer_token_materialize_token_2022_state_snapshot( + "account111", + 7, + &state, + ); + let second = crate::materializer_token_materialize_token_2022_state_snapshot( + "account111", + 7, + &state, + ); + assert_eq!(first, second); + assert!( + crate::materializer_token_materialize_token_2022_state_snapshot("", 7, &state).is_err() + ); + } +} diff --git a/kb-lib/src/materializer/token/constants.rs b/kb-lib/src/materializer/token/constants.rs deleted file mode 100644 index ddd8276..0000000 --- a/kb-lib/src/materializer/token/constants.rs +++ /dev/null @@ -1,11 +0,0 @@ -// file: kb-lib/src/materializer/token/constants.rs -// version: 3 - -//! Local constants for classic SPL Token account projections. - -/// Stable processor name persisted by the replay ledger. -pub(crate) const MT_TOKEN_ACCOUNTS_PROCESSOR_NAME: &str = "materializer.token.accounts"; -/// Stable projection payload contract version. -pub(crate) const MT_TOKEN_ACCOUNTS_PROJECTION_VERSION: u32 = 3; -/// Canonical tracing target for this component. -pub(crate) const TRACING_TARGET_MATERIALIZER_TOKEN_ACCOUNTS: &str = "kb-lib.materializer.token"; diff --git a/kb-lib/src/materializer/token/metadata_risk.rs b/kb-lib/src/materializer/token/metadata_risk.rs index ebafcef..b1c128b 100644 --- a/kb-lib/src/materializer/token/metadata_risk.rs +++ b/kb-lib/src/materializer/token/metadata_risk.rs @@ -1,61 +1,17 @@ // file: kb-lib/src/materializer/token/metadata_risk.rs -// version: 4 +// version: 5 -//! Reserved token-metadata risk materializer. +//! Token-metadata risk materializer component facade. -/// Reserved materializer for token-metadata risk observations. -#[derive(Clone, Debug, Default)] -pub struct MtTokenMetadataRiskMaterializer; +mod constants; +mod materializer; -impl crate::MtMaterializer for crate::MtTokenMetadataRiskMaterializer { - fn materializer_name(&self) -> &'static str { - return "kb-lib.materializer.token.metadata_risk"; - } +/// Reserved token-metadata risk materializer. +pub use self::materializer::MtTokenMetadataRiskMaterializer; - fn materializer_version(&self) -> &'static str { - return env!("CARGO_PKG_VERSION"); - } - - fn accepts_event(&self, _event: &crate::MdDecodedProtocolEvent) -> bool { - return false; - } - - fn materialize_event( - &self, - event: &crate::MdDecodedProtocolEvent, - ) -> kb_core::Result> { - return std::result::Result::Ok(std::vec![crate::MdMaterializedEvent { - signature: event.signature.clone(), - slot: event.slot, - decoded_family: event.event_family, - materialized_family: crate::MdMaterializedEventFamily::TokenMetadataRisk, - }]); - } -} - -impl crate::MtApiEventMaterializer for crate::MtTokenMetadataRiskMaterializer { - fn identity(&self) -> crate::MtApiMaterializerIdentity { - return crate::MtApiMaterializerIdentity { - name: "kb-lib.materializer.token.metadata_risk".to_string(), - version: env!("CARGO_PKG_VERSION").to_string(), - }; - } - - fn accepted_families(&self) -> &'static [crate::MdEventFamily] { - return &[]; - } - - fn transaction_policy( - &self, - _family: crate::MdEventFamily, - ) -> crate::MtApiMaterializationTransactionPolicy { - return crate::MtApiMaterializationTransactionPolicy::SuccessfulCommittedOnly; - } - - fn materialize( - &self, - _observation: &crate::DcApiDecodedObservation, - ) -> crate::MtApiMaterializerExecutionResult { - return crate::MtApiMaterializerExecutionResult::ignored(); - } -} +/// Empty accepted-family boundary for the reserved component. +pub(crate) use self::constants::MT_TOKEN_METADATA_RISK_ACCEPTED_FAMILIES; +/// Stable runtime component name for the reserved token-metadata risk materializer. +pub(crate) use self::constants::MT_TOKEN_METADATA_RISK_COMPONENT_NAME; +/// Stable processor name for the reserved token-metadata risk materializer. +pub(crate) use self::constants::MT_TOKEN_METADATA_RISK_PROCESSOR_NAME; diff --git a/kb-lib/src/materializer/token/metadata_risk/constants.rs b/kb-lib/src/materializer/token/metadata_risk/constants.rs new file mode 100644 index 0000000..e9edafc --- /dev/null +++ b/kb-lib/src/materializer/token/metadata_risk/constants.rs @@ -0,0 +1,12 @@ +// file: kb-lib/src/materializer/token/metadata_risk/constants.rs +// version: 2 + +//! Local constants for the token metadata_risk materializer. + +/// Stable runtime component name for the token metadata_risk materializer. +pub(crate) const MT_TOKEN_METADATA_RISK_COMPONENT_NAME: &str = + "kb-lib.materializer.token.metadata_risk"; +/// Stable processor name persisted by the replay ledger. +pub(crate) const MT_TOKEN_METADATA_RISK_PROCESSOR_NAME: &str = "materializer.token.metadata_risk"; +/// Empty family boundary retained while the materializer is reserved. +pub(crate) const MT_TOKEN_METADATA_RISK_ACCEPTED_FAMILIES: &[crate::MdEventFamily] = &[]; diff --git a/kb-lib/src/materializer/token/metadata_risk/materializer.rs b/kb-lib/src/materializer/token/metadata_risk/materializer.rs new file mode 100644 index 0000000..f4219b1 --- /dev/null +++ b/kb-lib/src/materializer/token/metadata_risk/materializer.rs @@ -0,0 +1,56 @@ +// file: kb-lib/src/materializer/token/metadata_risk/materializer.rs +// version: 5 + +//! Reserved token-metadata risk materializer. + +/// Reserved materializer for token-metadata risk observations. +#[derive(Clone, Debug, Default)] +pub struct MtTokenMetadataRiskMaterializer; + +impl crate::MtMaterializer for crate::MtTokenMetadataRiskMaterializer { + fn materializer_name(&self) -> &'static str { + return crate::MT_TOKEN_METADATA_RISK_COMPONENT_NAME; + } + + fn materializer_version(&self) -> &'static str { + return env!("CARGO_PKG_VERSION"); + } + + fn accepts_event(&self, _event: &crate::MdDecodedProtocolEvent) -> bool { + return false; + } + + fn materialize_event( + &self, + _event: &crate::MdDecodedProtocolEvent, + ) -> kb_core::Result> { + return std::result::Result::Ok(std::vec::Vec::new()); + } +} + +impl crate::MtApiEventMaterializer for crate::MtTokenMetadataRiskMaterializer { + fn identity(&self) -> crate::MtApiMaterializerIdentity { + return crate::MtApiMaterializerIdentity { + name: crate::MT_TOKEN_METADATA_RISK_PROCESSOR_NAME.to_string(), + version: env!("CARGO_PKG_VERSION").to_string(), + }; + } + + fn accepted_families(&self) -> &'static [crate::MdEventFamily] { + return crate::MT_TOKEN_METADATA_RISK_ACCEPTED_FAMILIES; + } + + fn transaction_policy( + &self, + _family: crate::MdEventFamily, + ) -> crate::MtApiMaterializationTransactionPolicy { + return crate::MtApiMaterializationTransactionPolicy::SuccessfulCommittedOnly; + } + + fn materialize( + &self, + _observation: &crate::DcApiDecodedObservation, + ) -> crate::MtApiMaterializerExecutionResult { + return crate::MtApiMaterializerExecutionResult::ignored(); + } +} diff --git a/kb-lib/src/materializer/trades.rs b/kb-lib/src/materializer/trades.rs index 1c8b57e..b581959 100644 --- a/kb-lib/src/materializer/trades.rs +++ b/kb-lib/src/materializer/trades.rs @@ -1,9 +1,17 @@ // file: kb-lib/src/materializer/trades.rs -// version: 3 +// version: 4 //! `trades` materializer family. -mod core; +mod constants; +mod materializer; /// Reserved trades materializer. -pub use self::core::MtTradesMaterializer; +pub use self::materializer::MtTradesMaterializer; + +/// Empty accepted-family boundary for the reserved component. +pub(crate) use self::constants::MT_TRADES_ACCEPTED_FAMILIES; +/// Stable runtime component name for the `trades` materializer. +pub(crate) use self::constants::MT_TRADES_COMPONENT_NAME; +/// Stable processor name for the `trades` materializer. +pub(crate) use self::constants::MT_TRADES_PROCESSOR_NAME; diff --git a/kb-lib/src/materializer/trades/constants.rs b/kb-lib/src/materializer/trades/constants.rs new file mode 100644 index 0000000..fbf196c --- /dev/null +++ b/kb-lib/src/materializer/trades/constants.rs @@ -0,0 +1,11 @@ +// file: kb-lib/src/materializer/trades/constants.rs +// version: 1 + +//! Local constants for the trades materializer. + +/// Stable runtime component name for the trades materializer. +pub(crate) const MT_TRADES_COMPONENT_NAME: &str = "kb-lib.materializer.trades"; +/// Stable processor name persisted by the replay ledger. +pub(crate) const MT_TRADES_PROCESSOR_NAME: &str = "materializer.trades"; +/// Empty family boundary retained while the materializer is reserved. +pub(crate) const MT_TRADES_ACCEPTED_FAMILIES: &[crate::MdEventFamily] = &[]; diff --git a/kb-lib/src/materializer/trades/core.rs b/kb-lib/src/materializer/trades/materializer.rs similarity index 69% rename from kb-lib/src/materializer/trades/core.rs rename to kb-lib/src/materializer/trades/materializer.rs index f28b31d..af87efc 100644 --- a/kb-lib/src/materializer/trades/core.rs +++ b/kb-lib/src/materializer/trades/materializer.rs @@ -1,15 +1,15 @@ -// file: kb-lib/src/materializer/trades/core.rs -// version: 4 +// file: kb-lib/src/materializer/trades/materializer.rs +// version: 5 //! Reserved trades materializer. -/// Reserved materializer for trade observations. +/// Reserved materializer for trades observations. #[derive(Clone, Debug, Default)] pub struct MtTradesMaterializer; impl crate::MtMaterializer for crate::MtTradesMaterializer { fn materializer_name(&self) -> &'static str { - return "kb-lib.materializer.trades"; + return crate::MT_TRADES_COMPONENT_NAME; } fn materializer_version(&self) -> &'static str { @@ -22,27 +22,22 @@ impl crate::MtMaterializer for crate::MtTradesMaterializer { fn materialize_event( &self, - event: &crate::MdDecodedProtocolEvent, + _event: &crate::MdDecodedProtocolEvent, ) -> kb_core::Result> { - return std::result::Result::Ok(std::vec![crate::MdMaterializedEvent { - signature: event.signature.clone(), - slot: event.slot, - decoded_family: event.event_family, - materialized_family: crate::MdMaterializedEventFamily::Trade, - }]); + return std::result::Result::Ok(std::vec::Vec::new()); } } impl crate::MtApiEventMaterializer for crate::MtTradesMaterializer { fn identity(&self) -> crate::MtApiMaterializerIdentity { return crate::MtApiMaterializerIdentity { - name: "kb-lib.materializer.trades".to_string(), + name: crate::MT_TRADES_PROCESSOR_NAME.to_string(), version: env!("CARGO_PKG_VERSION").to_string(), }; } fn accepted_families(&self) -> &'static [crate::MdEventFamily] { - return &[]; + return crate::MT_TRADES_ACCEPTED_FAMILIES; } fn transaction_policy( diff --git a/kb-lib/src/materializer/transaction.rs b/kb-lib/src/materializer/transaction.rs index 9933d0b..22c2c09 100644 --- a/kb-lib/src/materializer/transaction.rs +++ b/kb-lib/src/materializer/transaction.rs @@ -1,19 +1,24 @@ // file: kb-lib/src/materializer/transaction.rs -// version: 3 +// version: 4 //! `transaction` materializer family. mod annotations; -mod constants; /// Stable committed Memo transaction annotation materializer. pub use self::annotations::MtTransactionAnnotationMaterializer; +/// Crate-visible `MT_TRANSACTION_ANNOTATIONS_ACCEPTED_FAMILIES` component constant. +pub(crate) use self::annotations::MT_TRANSACTION_ANNOTATIONS_ACCEPTED_FAMILIES; +/// Stable runtime component name for transaction annotations. +pub(crate) use self::annotations::MT_TRANSACTION_ANNOTATIONS_COMPONENT_NAME; /// Maximum Memo payload accepted by transaction annotations. -pub(crate) use self::constants::MT_TRANSACTION_ANNOTATIONS_MAX_MEMO_PAYLOAD_BYTES; +pub(crate) use self::annotations::MT_TRANSACTION_ANNOTATIONS_MAX_MEMO_PAYLOAD_BYTES; +/// Crate-visible `MT_TRANSACTION_ANNOTATIONS_MEMO_ENTRY_NAMES` component constant. +pub(crate) use self::annotations::MT_TRANSACTION_ANNOTATIONS_MEMO_ENTRY_NAMES; /// Stable processor name for transaction annotations. -pub(crate) use self::constants::MT_TRANSACTION_ANNOTATIONS_PROCESSOR_NAME; +pub(crate) use self::annotations::MT_TRANSACTION_ANNOTATIONS_PROCESSOR_NAME; /// Stable projection contract version for transaction annotations. -pub(crate) use self::constants::MT_TRANSACTION_ANNOTATIONS_PROJECTION_VERSION; +pub(crate) use self::annotations::MT_TRANSACTION_ANNOTATIONS_PROJECTION_VERSION; /// Canonical tracing target for transaction annotations. -pub(crate) use self::constants::TRACING_TARGET_MATERIALIZER_TRANSACTION_ANNOTATIONS; +pub(crate) use self::annotations::TRACING_TARGET_MATERIALIZER_TRANSACTION_ANNOTATIONS; diff --git a/kb-lib/src/materializer/transaction/annotations.rs b/kb-lib/src/materializer/transaction/annotations.rs index 3b8d742..5a140fa 100644 --- a/kb-lib/src/materializer/transaction/annotations.rs +++ b/kb-lib/src/materializer/transaction/annotations.rs @@ -1,611 +1,25 @@ // file: kb-lib/src/materializer/transaction/annotations.rs -// version: 5 +// version: 6 -//! Exact committed SPL Memo transaction annotation projection. +//! Committed Memo transaction annotation materializer component. -const ACCEPTED_FAMILIES: &[crate::MdEventFamily] = &[crate::MdEventFamily::Audit]; -const MEMO_ENTRY_NAMES: &[&str] = &["add_memo", "memo_intent", "invalid_memo_attempt"]; +mod constants; +mod materializer; -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum MemoGeneration { - V1, - V3, - V4, -} +/// Stable committed Memo transaction annotation materializer. +pub use self::materializer::MtTransactionAnnotationMaterializer; -impl MemoGeneration { - fn from_event(event: &crate::MdDecodedProtocolEvent) -> std::option::Option { - if event.program_id.0 == kb_program_ids::SPL_MEMO_V1_PROGRAM_ID - && event.surface_code.0 == "spl.memo.v1" - { - return std::option::Option::Some(Self::V1); - } - if event.program_id.0 == kb_program_ids::SPL_MEMO_V3_PROGRAM_ID - && event.surface_code.0 == "spl.memo.v3" - { - return std::option::Option::Some(Self::V3); - } - if event.program_id.0 == kb_program_ids::SPL_MEMO_V4_PROGRAM_ID - && event.surface_code.0 == "spl.memo.v4" - { - return std::option::Option::Some(Self::V4); - } - return std::option::Option::None; - } - - fn code(self) -> &'static str { - return match self { - Self::V1 => "v1", - Self::V3 => "v3", - Self::V4 => "v4", - }; - } - - fn signer_status(self) -> &'static str { - return match self { - Self::V1 => "not_applicable", - Self::V3 | Self::V4 => "valid", - }; - } -} - -struct MemoProjection { - generation: MemoGeneration, - memo_text: std::string::String, - payload_length_bytes: u64, - payload_sha256: std::string::String, - required_signers: std::vec::Vec, - observed_signers: std::vec::Vec, - verified_signers: std::vec::Vec, - signer_validation_status: std::string::String, -} - -/// Stable transaction annotation materializer. -#[derive(Clone, Debug, Default)] -pub struct MtTransactionAnnotationMaterializer; - -impl crate::MtMaterializer for crate::MtTransactionAnnotationMaterializer { - fn materializer_name(&self) -> &'static str { - return "kb-lib.materializer.transaction.annotations"; - } - - fn materializer_version(&self) -> &'static str { - return env!("CARGO_PKG_VERSION"); - } - - fn accepts_event(&self, event: &crate::MdDecodedProtocolEvent) -> bool { - return accepts_event(event); - } - - fn materialize_event( - &self, - _event: &crate::MdDecodedProtocolEvent, - ) -> kb_core::Result> { - return std::result::Result::Ok(std::vec::Vec::new()); - } -} - -impl crate::MtApiEventMaterializer for crate::MtTransactionAnnotationMaterializer { - fn identity(&self) -> crate::MtApiMaterializerIdentity { - return crate::MtApiMaterializerIdentity { - name: crate::MT_TRANSACTION_ANNOTATIONS_PROCESSOR_NAME.to_string(), - version: env!("CARGO_PKG_VERSION").to_string(), - }; - } - - fn accepted_families(&self) -> &'static [crate::MdEventFamily] { - return ACCEPTED_FAMILIES; - } - - fn accepts_observation(&self, observation: &crate::DcApiDecodedObservation) -> bool { - return accepts_event(&observation.event); - } - - fn transaction_policy( - &self, - _family: crate::MdEventFamily, - ) -> crate::MtApiMaterializationTransactionPolicy { - return crate::MtApiMaterializationTransactionPolicy::SuccessfulCommittedOnly; - } - - fn materialize( - &self, - observation: &crate::DcApiDecodedObservation, - ) -> crate::MtApiMaterializerExecutionResult { - if !accepts_event(&observation.event) { - return crate::MtApiMaterializerExecutionResult::ignored(); - } - if observation.transaction_failed || !observation.observation_committed { - return crate::MtApiMaterializerExecutionResult::refused( - "uncommitted_transaction_annotation_refused", - "failed or uncommitted Memo observations cannot create transaction annotations", - ); - } - if observation.event.event_name.0 != "add_memo" { - return crate::MtApiMaterializerExecutionResult::refused( - "invalid_transaction_annotation_source_refused", - "only a committed add_memo observation can create a transaction annotation", - ); - } - let projection_result = parse_projection(observation); - let projection = match projection_result { - std::result::Result::Ok(value) => value, - std::result::Result::Err(message) => { - return failed("memo_annotation_payload_invalid", message); - }, - }; - let idempotence_key = format!( - "spl_memo:{}:{}:{}:{}", - observation.event.signature.0, - observation.event.instruction_path.0, - observation.event.program_id.0, - projection.payload_sha256 - ); - let output = crate::MtApiMaterializedOutput { - output_key: "transaction_annotation:spl_memo:0".to_string(), - family: crate::MdMaterializedEventFamily::TransactionAnnotation, - payload_json: serde_json::json!({ - "projectionVersion": crate::MT_TRANSACTION_ANNOTATIONS_PROJECTION_VERSION, - "domain": "transaction_annotation", - "annotationKind": "spl.memo", - "idempotenceKey": idempotence_key, - "signature": observation.event.signature.0.clone(), - "slot": observation.event.slot.0, - "instructionPath": observation.event.instruction_path.0.clone(), - "sourceKind": match observation.event.source_kind { - crate::MdEventSourceKind::InnerInstruction => "inner_instruction", - _ => "instruction" - }, - "generation": projection.generation.code(), - "programId": observation.event.program_id.0.clone(), - "text": projection.memo_text, - "payloadLengthBytes": projection.payload_length_bytes, - "payloadSha256": projection.payload_sha256, - "signers": { - "validationStatus": projection.signer_validation_status, - "required": projection.required_signers, - "observed": projection.observed_signers, - "verified": projection.verified_signers - }, - "committed": true, - "provenance": { - "processorName": crate::MT_TRANSACTION_ANNOTATIONS_PROCESSOR_NAME, - "processorVersion": env!("CARGO_PKG_VERSION"), - "sourceProtocol": observation.event.protocol_code.0.clone(), - "sourceSurface": observation.event.surface_code.0.clone(), - "sourceEventCode": observation.event.event_code.0.clone(), - "sourceEventKey": observation.event_key.clone() - } - }), - }; - tracing::debug!( - target: crate::TRACING_TARGET_MATERIALIZER_TRANSACTION_ANNOTATIONS, - action = "materialize_transaction_annotation", - signature = %observation.event.signature.0, - instruction_path = %observation.event.instruction_path.0, - program_id = %observation.event.program_id.0, - generation = projection.generation.code(), - payload_length_bytes = projection.payload_length_bytes, - output_count = 1_usize, - "materialize committed SPL Memo transaction annotation" - ); - return crate::MtApiMaterializerExecutionResult { - status: crate::MtApiMaterializerOutcomeStatus::Inserted, - outputs: std::vec![output], - diagnostics: std::vec::Vec::new(), - }; - } -} - -fn accepts_event(event: &crate::MdDecodedProtocolEvent) -> bool { - return event.event_family == crate::MdEventFamily::Audit - && event.protocol_code.0 == "spl.memo" - && (event.source_kind == crate::MdEventSourceKind::Instruction - || event.source_kind == crate::MdEventSourceKind::InnerInstruction) - && MemoGeneration::from_event(event).is_some() - && MEMO_ENTRY_NAMES.contains(&event.event_name.0.as_str()); -} - -fn parse_projection( - observation: &crate::DcApiDecodedObservation, -) -> std::result::Result { - let generation = match MemoGeneration::from_event(&observation.event) { - std::option::Option::Some(value) => value, - std::option::Option::None => { - return std::result::Result::Err( - "Memo generation and Program ID do not match".to_string(), - ); - }, - }; - let payload = &observation.payload_json; - if payload.get("committed").and_then(serde_json::Value::as_bool) - != std::option::Option::Some(true) - { - return std::result::Result::Err("Memo payload is not marked committed".to_string()); - } - if payload.get("transactionSucceeded").and_then(serde_json::Value::as_bool) - != std::option::Option::Some(true) - || payload.get("payloadComplete").and_then(serde_json::Value::as_bool) - != std::option::Option::Some(true) - || string_field(payload, "runtimeValidation") - != std::option::Option::Some("proven_by_successful_transaction") - || payload - .get("utf8Validation") - .and_then(|value| return string_field(value, "status")) - != std::option::Option::Some("valid") - { - return std::result::Result::Err( - "Memo successful runtime validation proof is incomplete".to_string(), - ); - } - if string_field(payload, "generation") != std::option::Option::Some(generation.code()) { - return std::result::Result::Err( - "Memo payload generation does not match its surface".to_string(), - ); - } - if string_field(payload, "programId") - != std::option::Option::Some(observation.event.program_id.0.as_str()) - { - return std::result::Result::Err( - "Memo payload Program ID does not match its event".to_string(), - ); - } - if string_field(payload, "instructionPath") - != std::option::Option::Some(observation.event.instruction_path.0.as_str()) - { - return std::result::Result::Err( - "Memo payload instruction path does not match its event".to_string(), - ); - } - let memo_text = match string_field(payload, "memoText") { - std::option::Option::Some(value) => value.to_string(), - std::option::Option::None => { - return std::result::Result::Err("committed Memo has no UTF-8 text".to_string()); - }, - }; - let payload_length_bytes = - match payload.get("payloadLengthBytes").and_then(serde_json::Value::as_u64) { - std::option::Option::Some(value) => value, - std::option::Option::None => { - return std::result::Result::Err("committed Memo has no byte length".to_string()); - }, - }; - let measured_length = match u64::try_from(memo_text.len()) { - std::result::Result::Ok(value) => value, - std::result::Result::Err(error) => { - return std::result::Result::Err(format!( - "Memo text length conversion failed: {error}" - )); - }, - }; - if measured_length != payload_length_bytes { - return std::result::Result::Err( - "Memo byte length does not match its UTF-8 text".to_string(), - ); - } - if payload_length_bytes > crate::MT_TRANSACTION_ANNOTATIONS_MAX_MEMO_PAYLOAD_BYTES { - return std::result::Result::Err( - "Memo byte length exceeds the projection limit".to_string(), - ); - } - let payload_sha256 = match string_field(payload, "payloadSha256") { - std::option::Option::Some(value) if is_sha256(value) => value.to_string(), - _ => return std::result::Result::Err("Memo payload SHA-256 is invalid".to_string()), - }; - let signer_validation = match payload.get("signerValidation") { - std::option::Option::Some(value) => value, - std::option::Option::None => { - return std::result::Result::Err("Memo signer validation is absent".to_string()); - }, - }; - let signer_validation_status = match string_field(signer_validation, "status") { - std::option::Option::Some(value) if value == generation.signer_status() => { - value.to_string() - }, - _ => { - return std::result::Result::Err( - "Memo signer validation status is inconsistent".to_string(), - ); - }, - }; - let missing_signers = match string_array(signer_validation, "missingRequiredSigners") { - std::result::Result::Ok(value) => value, - std::result::Result::Err(message) => return std::result::Result::Err(message), - }; - if !missing_signers.is_empty() { - return std::result::Result::Err("committed Memo reports missing signers".to_string()); - } - let required_signers = match string_array(signer_validation, "orderedRequiredSigners") { - std::result::Result::Ok(value) => value, - std::result::Result::Err(message) => return std::result::Result::Err(message), - }; - let observed_signers = match string_array(signer_validation, "orderedObservedSigners") { - std::result::Result::Ok(value) => value, - std::result::Result::Err(message) => return std::result::Result::Err(message), - }; - let verified_signers = if generation == MemoGeneration::V1 { - if !required_signers.is_empty() { - return std::result::Result::Err("Memo v1 cannot require signers".to_string()); - } - std::vec::Vec::new() - } else { - if required_signers != observed_signers { - return std::result::Result::Err( - "committed Memo required and observed signers differ".to_string(), - ); - } - observed_signers.clone() - }; - return std::result::Result::Ok(MemoProjection { - generation, - memo_text, - payload_length_bytes, - payload_sha256, - required_signers, - observed_signers, - verified_signers, - signer_validation_status, - }); -} - -fn string_field<'a>(value: &'a serde_json::Value, key: &str) -> std::option::Option<&'a str> { - return value.get(key).and_then(serde_json::Value::as_str); -} - -fn string_array( - value: &serde_json::Value, - key: &str, -) -> std::result::Result, std::string::String> { - let values = match value.get(key).and_then(serde_json::Value::as_array) { - std::option::Option::Some(values) => values, - std::option::Option::None => { - return std::result::Result::Err(format!("Memo signer field {key} is not an array")); - }, - }; - let mut output = std::vec::Vec::with_capacity(values.len()); - for item in values { - let text = match item.as_str() { - std::option::Option::Some(text) if !text.trim().is_empty() => text, - _ => { - return std::result::Result::Err(format!( - "Memo signer field {key} contains an invalid pubkey" - )); - }, - }; - output.push(text.to_string()); - } - return std::result::Result::Ok(output); -} - -fn is_sha256(value: &str) -> bool { - return value.len() == 64 - && value - .as_bytes() - .iter() - .all(|byte| return byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase()); -} - -fn failed(code: &str, message: std::string::String) -> crate::MtApiMaterializerExecutionResult { - return crate::MtApiMaterializerExecutionResult { - status: crate::MtApiMaterializerOutcomeStatus::Failed, - outputs: std::vec::Vec::new(), - diagnostics: std::vec![crate::MtApiMaterializerDiagnostic { - code: code.to_string(), - message, - retriable: false, - }], - }; -} - -#[cfg(test)] -mod tests { - fn observation( - program_id: &str, - surface_code: &str, - generation: &str, - event_name: &str, - failed: bool, - committed: bool, - required_signers: serde_json::Value, - observed_signers: serde_json::Value, - ) -> crate::DcApiDecodedObservation { - return crate::DcApiDecodedObservation { - event_key: "memo:0".to_string(), - event: crate::MdDecodedProtocolEvent { - signature: crate::MdSignature("signature".to_string()), - slot: crate::MdSlot(42), - instruction_path: crate::MdInstructionPath("1/0".to_string()), - program_id: crate::MdProgramId(program_id.to_string()), - protocol_code: crate::MdProtocolCode("spl.memo".to_string()), - surface_code: crate::MdSurfaceCode(surface_code.to_string()), - event_code: crate::MdEventCode(format!("{surface_code}.{event_name}")), - event_name: crate::MdEventName(event_name.to_string()), - event_family: crate::MdEventFamily::Audit, - source_kind: crate::MdEventSourceKind::InnerInstruction, - confidence: crate::MdDecoderConfidence::ManualExact, - }, - payload_json: serde_json::json!({ - "generation": generation, - "programId": program_id, - "instructionPath": "1/0", - "transactionSucceeded": !failed, - "committed": committed, - "memoText": "memo", - "payloadLengthBytes": 4, - "payloadSha256": "1111111111111111111111111111111111111111111111111111111111111111", - "payloadComplete": true, - "utf8Validation": {"status":"valid","invalidFromByte":null}, - "signerValidation": { - "status": if generation == "v1" { "not_applicable" } else { "valid" }, - "orderedRequiredSigners": required_signers, - "orderedObservedSigners": observed_signers, - "missingRequiredSigners": [] - }, - "runtimeValidation": if failed { - "not_proven_transaction_failed" - } else { - "proven_by_successful_transaction" - } - }), - transaction_failed: failed, - transaction_error: if failed { - std::option::Option::Some(serde_json::json!({"InstructionError":[0,"Custom"]})) - } else { - std::option::Option::None - }, - observation_committed: committed, - proof: crate::DcApiDecoderProof { - kind: crate::DcApiDecoderProofKind::Manual, - confidence: crate::MdDecoderConfidence::ManualExact, - evidence: std::vec!["memo".to_string()], - }, - }; - } - - fn v4() -> crate::DcApiDecodedObservation { - return observation( - kb_program_ids::SPL_MEMO_V4_PROGRAM_ID, - "spl.memo.v4", - "v4", - "add_memo", - false, - true, - serde_json::json!(["signer", "signer"]), - serde_json::json!(["signer", "signer"]), - ); - } - - #[test] - fn committed_v4_memo_becomes_deterministic_transaction_annotation() { - let materializer = crate::MtTransactionAnnotationMaterializer; - let first = crate::MtApiEventMaterializer::materialize(&materializer, &v4()); - let second = crate::MtApiEventMaterializer::materialize(&materializer, &v4()); - assert_eq!(first, second); - assert_eq!(first.status, crate::MtApiMaterializerOutcomeStatus::Inserted); - assert_eq!(first.outputs.len(), 1); - assert_eq!( - first.outputs[0].family, - crate::MdMaterializedEventFamily::TransactionAnnotation - ); - assert_eq!( - first.outputs[0].payload_json["signers"]["verified"], - serde_json::json!(["signer", "signer"]) - ); - assert_eq!(first.outputs[0].payload_json["committed"], true); - assert!(first.validate().is_ok()); - } - - #[test] - fn exact_v1_v3_and_v4_surfaces_are_accepted() { - for (program_id, surface, generation) in [ - (kb_program_ids::SPL_MEMO_V1_PROGRAM_ID, "spl.memo.v1", "v1"), - (kb_program_ids::SPL_MEMO_V3_PROGRAM_ID, "spl.memo.v3", "v3"), - (kb_program_ids::SPL_MEMO_V4_PROGRAM_ID, "spl.memo.v4", "v4"), - ] { - let observation = observation( - program_id, - surface, - generation, - "add_memo", - false, - true, - serde_json::json!([]), - serde_json::json!([]), - ); - assert!(crate::MtApiEventMaterializer::accepts_observation( - &crate::MtTransactionAnnotationMaterializer, - &observation, - )); - } - } - - #[test] - fn v1_observed_accounts_are_not_claimed_as_runtime_verified() { - let observation = observation( - kb_program_ids::SPL_MEMO_V1_PROGRAM_ID, - "spl.memo.v1", - "v1", - "add_memo", - false, - true, - serde_json::json!([]), - serde_json::json!(["observed_only"]), - ); - let result = crate::MtApiEventMaterializer::materialize( - &crate::MtTransactionAnnotationMaterializer, - &observation, - ); - assert_eq!(result.outputs[0].payload_json["signers"]["verified"], serde_json::json!([])); - assert_eq!( - result.outputs[0].payload_json["signers"]["observed"], - serde_json::json!(["observed_only"]) - ); - } - - #[test] - fn failed_or_uncommitted_memo_is_refused_without_output() { - let observation = observation( - kb_program_ids::SPL_MEMO_V4_PROGRAM_ID, - "spl.memo.v4", - "v4", - "memo_intent", - true, - false, - serde_json::json!([]), - serde_json::json!([]), - ); - let materializer = crate::MtTransactionAnnotationMaterializer; - let policy = - crate::materializer_api_validate_materialization_policy(&materializer, &observation); - assert!(policy.is_err()); - let result = crate::MtApiEventMaterializer::materialize(&materializer, &observation); - assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Refused); - assert!(result.outputs.is_empty()); - } - - #[test] - fn malformed_committed_payload_fails_closed() { - let mut observation = v4(); - observation.payload_json["payloadSha256"] = serde_json::Value::String("bad".to_string()); - let result = crate::MtApiEventMaterializer::materialize( - &crate::MtTransactionAnnotationMaterializer, - &observation, - ); - assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Failed); - assert!(result.outputs.is_empty()); - assert_eq!(result.diagnostics[0].code, "memo_annotation_payload_invalid"); - } - - #[test] - fn foreign_audit_observation_is_not_accepted() { - let mut observation = v4(); - observation.event.protocol_code = crate::MdProtocolCode("foreign".to_string()); - assert!(!crate::MtApiEventMaterializer::accepts_observation( - &crate::MtTransactionAnnotationMaterializer, - &observation, - )); - let result = crate::MtApiEventMaterializer::materialize( - &crate::MtTransactionAnnotationMaterializer, - &observation, - ); - assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Ignored); - } - - #[test] - fn machine_readable_matrix_declares_transaction_annotation_policy() { - let parsed = serde_json::from_str::(include_str!( - "../../../../test-fixtures/contract-matrices/SPL_MEMO_MATRIX.json" - )); - let matrix = match parsed { - std::result::Result::Ok(value) => value, - std::result::Result::Err(error) => panic!("memo matrix is invalid JSON: {error}"), - }; - assert_eq!(matrix["matrixVersion"], 4); - assert_eq!(matrix["materializationContract"]["crate"], "kb-lib"); - assert_eq!( - matrix["materializationContract"]["transactionPolicy"], - "SuccessfulCommittedOnly" - ); - assert_eq!(matrix["materializationContract"]["failedOrUncommittedOutput"], false); - } -} +/// Crate-visible `MT_TRANSACTION_ANNOTATIONS_ACCEPTED_FAMILIES` component constant. +pub(crate) use self::constants::MT_TRANSACTION_ANNOTATIONS_ACCEPTED_FAMILIES; +/// Stable runtime component name for transaction annotations. +pub(crate) use self::constants::MT_TRANSACTION_ANNOTATIONS_COMPONENT_NAME; +/// Maximum Memo payload accepted by transaction annotations. +pub(crate) use self::constants::MT_TRANSACTION_ANNOTATIONS_MAX_MEMO_PAYLOAD_BYTES; +/// Crate-visible `MT_TRANSACTION_ANNOTATIONS_MEMO_ENTRY_NAMES` component constant. +pub(crate) use self::constants::MT_TRANSACTION_ANNOTATIONS_MEMO_ENTRY_NAMES; +/// Stable processor name for transaction annotations. +pub(crate) use self::constants::MT_TRANSACTION_ANNOTATIONS_PROCESSOR_NAME; +/// Stable projection contract version for transaction annotations. +pub(crate) use self::constants::MT_TRANSACTION_ANNOTATIONS_PROJECTION_VERSION; +/// Canonical tracing target for transaction annotations. +pub(crate) use self::constants::TRACING_TARGET_MATERIALIZER_TRANSACTION_ANNOTATIONS; diff --git a/kb-lib/src/materializer/transaction/annotations/constants.rs b/kb-lib/src/materializer/transaction/annotations/constants.rs new file mode 100644 index 0000000..cb0ea73 --- /dev/null +++ b/kb-lib/src/materializer/transaction/annotations/constants.rs @@ -0,0 +1,25 @@ +// file: kb-lib/src/materializer/transaction/annotations/constants.rs +// version: 5 + +//! Local constants for the transaction annotations materializer. + +/// Stable runtime component name for the transaction annotations materializer. +pub(crate) const MT_TRANSACTION_ANNOTATIONS_COMPONENT_NAME: &str = + "kb-lib.materializer.transaction.annotations"; +/// Stable processor name persisted by the replay ledger. +pub(crate) const MT_TRANSACTION_ANNOTATIONS_PROCESSOR_NAME: &str = + "materializer.transaction.annotations"; +/// Maximum Memo payload accepted by transaction annotations. +pub(crate) const MT_TRANSACTION_ANNOTATIONS_MAX_MEMO_PAYLOAD_BYTES: u64 = 566; +/// Stable projection contract version for the transaction annotations materializer. +pub(crate) const MT_TRANSACTION_ANNOTATIONS_PROJECTION_VERSION: u32 = 1; +/// Canonical tracing target for the transaction annotations materializer. +pub(crate) const TRACING_TARGET_MATERIALIZER_TRANSACTION_ANNOTATIONS: &str = + "kb-lib.materializer.transaction.annotations"; + +/// Component contract for `accepted_families`. +pub(crate) const MT_TRANSACTION_ANNOTATIONS_ACCEPTED_FAMILIES: &[crate::MdEventFamily] = + &[crate::MdEventFamily::Audit]; +/// Component contract for `memo_entry_names`. +pub(crate) const MT_TRANSACTION_ANNOTATIONS_MEMO_ENTRY_NAMES: &[&str] = + &["add_memo", "memo_intent", "invalid_memo_attempt"]; diff --git a/kb-lib/src/materializer/transaction/annotations/materializer.rs b/kb-lib/src/materializer/transaction/annotations/materializer.rs new file mode 100644 index 0000000..bf48632 --- /dev/null +++ b/kb-lib/src/materializer/transaction/annotations/materializer.rs @@ -0,0 +1,609 @@ +// file: kb-lib/src/materializer/transaction/annotations/materializer.rs +// version: 7 + +//! Exact committed SPL Memo transaction annotation projection. + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum MemoGeneration { + V1, + V3, + V4, +} + +impl MemoGeneration { + fn from_event(event: &crate::MdDecodedProtocolEvent) -> std::option::Option { + if event.program_id.0 == kb_program_ids::SPL_MEMO_V1_PROGRAM_ID + && event.surface_code.0 == "spl.memo.v1" + { + return std::option::Option::Some(Self::V1); + } + if event.program_id.0 == kb_program_ids::SPL_MEMO_V3_PROGRAM_ID + && event.surface_code.0 == "spl.memo.v3" + { + return std::option::Option::Some(Self::V3); + } + if event.program_id.0 == kb_program_ids::SPL_MEMO_V4_PROGRAM_ID + && event.surface_code.0 == "spl.memo.v4" + { + return std::option::Option::Some(Self::V4); + } + return std::option::Option::None; + } + + fn code(self) -> &'static str { + return match self { + Self::V1 => "v1", + Self::V3 => "v3", + Self::V4 => "v4", + }; + } + + fn signer_status(self) -> &'static str { + return match self { + Self::V1 => "not_applicable", + Self::V3 | Self::V4 => "valid", + }; + } +} + +struct MemoProjection { + generation: MemoGeneration, + memo_text: std::string::String, + payload_length_bytes: u64, + payload_sha256: std::string::String, + required_signers: std::vec::Vec, + observed_signers: std::vec::Vec, + verified_signers: std::vec::Vec, + signer_validation_status: std::string::String, +} + +/// Stable transaction annotation materializer. +#[derive(Clone, Debug, Default)] +pub struct MtTransactionAnnotationMaterializer; + +impl crate::MtMaterializer for crate::MtTransactionAnnotationMaterializer { + fn materializer_name(&self) -> &'static str { + return crate::MT_TRANSACTION_ANNOTATIONS_COMPONENT_NAME; + } + + fn materializer_version(&self) -> &'static str { + return env!("CARGO_PKG_VERSION"); + } + + fn accepts_event(&self, event: &crate::MdDecodedProtocolEvent) -> bool { + return accepts_event(event); + } + + fn materialize_event( + &self, + _event: &crate::MdDecodedProtocolEvent, + ) -> kb_core::Result> { + return std::result::Result::Ok(std::vec::Vec::new()); + } +} + +impl crate::MtApiEventMaterializer for crate::MtTransactionAnnotationMaterializer { + fn identity(&self) -> crate::MtApiMaterializerIdentity { + return crate::MtApiMaterializerIdentity { + name: crate::MT_TRANSACTION_ANNOTATIONS_PROCESSOR_NAME.to_string(), + version: env!("CARGO_PKG_VERSION").to_string(), + }; + } + + fn accepted_families(&self) -> &'static [crate::MdEventFamily] { + return crate::MT_TRANSACTION_ANNOTATIONS_ACCEPTED_FAMILIES; + } + + fn accepts_observation(&self, observation: &crate::DcApiDecodedObservation) -> bool { + return accepts_event(&observation.event); + } + + fn transaction_policy( + &self, + _family: crate::MdEventFamily, + ) -> crate::MtApiMaterializationTransactionPolicy { + return crate::MtApiMaterializationTransactionPolicy::SuccessfulCommittedOnly; + } + + fn materialize( + &self, + observation: &crate::DcApiDecodedObservation, + ) -> crate::MtApiMaterializerExecutionResult { + if !accepts_event(&observation.event) { + return crate::MtApiMaterializerExecutionResult::ignored(); + } + if observation.transaction_failed || !observation.observation_committed { + return crate::MtApiMaterializerExecutionResult::refused( + "uncommitted_transaction_annotation_refused", + "failed or uncommitted Memo observations cannot create transaction annotations", + ); + } + if observation.event.event_name.0 != "add_memo" { + return crate::MtApiMaterializerExecutionResult::refused( + "invalid_transaction_annotation_source_refused", + "only a committed add_memo observation can create a transaction annotation", + ); + } + let projection_result = parse_projection(observation); + let projection = match projection_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(message) => { + return failed("memo_annotation_payload_invalid", message); + }, + }; + let idempotence_key = format!( + "spl_memo:{}:{}:{}:{}", + observation.event.signature.0, + observation.event.instruction_path.0, + observation.event.program_id.0, + projection.payload_sha256 + ); + let output = crate::MtApiMaterializedOutput { + output_key: "transaction_annotation:spl_memo:0".to_string(), + family: crate::MdMaterializedEventFamily::TransactionAnnotation, + payload_json: serde_json::json!({ + "projectionVersion": crate::MT_TRANSACTION_ANNOTATIONS_PROJECTION_VERSION, + "domain": "transaction_annotation", + "annotationKind": "spl.memo", + "idempotenceKey": idempotence_key, + "signature": observation.event.signature.0.clone(), + "slot": observation.event.slot.0, + "instructionPath": observation.event.instruction_path.0.clone(), + "sourceKind": match observation.event.source_kind { + crate::MdEventSourceKind::InnerInstruction => "inner_instruction", + _ => "instruction" + }, + "generation": projection.generation.code(), + "programId": observation.event.program_id.0.clone(), + "text": projection.memo_text, + "payloadLengthBytes": projection.payload_length_bytes, + "payloadSha256": projection.payload_sha256, + "signers": { + "validationStatus": projection.signer_validation_status, + "required": projection.required_signers, + "observed": projection.observed_signers, + "verified": projection.verified_signers + }, + "committed": true, + "provenance": { + "processorName": crate::MT_TRANSACTION_ANNOTATIONS_PROCESSOR_NAME, + "processorVersion": env!("CARGO_PKG_VERSION"), + "sourceProtocol": observation.event.protocol_code.0.clone(), + "sourceSurface": observation.event.surface_code.0.clone(), + "sourceEventCode": observation.event.event_code.0.clone(), + "sourceEventKey": observation.event_key.clone() + } + }), + }; + tracing::debug!( + target: crate::TRACING_TARGET_MATERIALIZER_TRANSACTION_ANNOTATIONS, + action = "materialize_transaction_annotation", + signature = %observation.event.signature.0, + instruction_path = %observation.event.instruction_path.0, + program_id = %observation.event.program_id.0, + generation = projection.generation.code(), + payload_length_bytes = projection.payload_length_bytes, + output_count = 1_usize, + "materialize committed SPL Memo transaction annotation" + ); + return crate::MtApiMaterializerExecutionResult { + status: crate::MtApiMaterializerOutcomeStatus::Inserted, + outputs: std::vec![output], + diagnostics: std::vec::Vec::new(), + }; + } +} + +fn accepts_event(event: &crate::MdDecodedProtocolEvent) -> bool { + return event.event_family == crate::MdEventFamily::Audit + && event.protocol_code.0 == "spl.memo" + && (event.source_kind == crate::MdEventSourceKind::Instruction + || event.source_kind == crate::MdEventSourceKind::InnerInstruction) + && MemoGeneration::from_event(event).is_some() + && crate::MT_TRANSACTION_ANNOTATIONS_MEMO_ENTRY_NAMES + .contains(&event.event_name.0.as_str()); +} + +fn parse_projection( + observation: &crate::DcApiDecodedObservation, +) -> std::result::Result { + let generation = match MemoGeneration::from_event(&observation.event) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err( + "Memo generation and Program ID do not match".to_string(), + ); + }, + }; + let payload = &observation.payload_json; + if payload.get("committed").and_then(serde_json::Value::as_bool) + != std::option::Option::Some(true) + { + return std::result::Result::Err("Memo payload is not marked committed".to_string()); + } + if payload.get("transactionSucceeded").and_then(serde_json::Value::as_bool) + != std::option::Option::Some(true) + || payload.get("payloadComplete").and_then(serde_json::Value::as_bool) + != std::option::Option::Some(true) + || string_field(payload, "runtimeValidation") + != std::option::Option::Some("proven_by_successful_transaction") + || payload + .get("utf8Validation") + .and_then(|value| return string_field(value, "status")) + != std::option::Option::Some("valid") + { + return std::result::Result::Err( + "Memo successful runtime validation proof is incomplete".to_string(), + ); + } + if string_field(payload, "generation") != std::option::Option::Some(generation.code()) { + return std::result::Result::Err( + "Memo payload generation does not match its surface".to_string(), + ); + } + if string_field(payload, "programId") + != std::option::Option::Some(observation.event.program_id.0.as_str()) + { + return std::result::Result::Err( + "Memo payload Program ID does not match its event".to_string(), + ); + } + if string_field(payload, "instructionPath") + != std::option::Option::Some(observation.event.instruction_path.0.as_str()) + { + return std::result::Result::Err( + "Memo payload instruction path does not match its event".to_string(), + ); + } + let memo_text = match string_field(payload, "memoText") { + std::option::Option::Some(value) => value.to_string(), + std::option::Option::None => { + return std::result::Result::Err("committed Memo has no UTF-8 text".to_string()); + }, + }; + let payload_length_bytes = + match payload.get("payloadLengthBytes").and_then(serde_json::Value::as_u64) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err("committed Memo has no byte length".to_string()); + }, + }; + let measured_length = match u64::try_from(memo_text.len()) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return std::result::Result::Err(format!( + "Memo text length conversion failed: {error}" + )); + }, + }; + if measured_length != payload_length_bytes { + return std::result::Result::Err( + "Memo byte length does not match its UTF-8 text".to_string(), + ); + } + if payload_length_bytes > crate::MT_TRANSACTION_ANNOTATIONS_MAX_MEMO_PAYLOAD_BYTES { + return std::result::Result::Err( + "Memo byte length exceeds the projection limit".to_string(), + ); + } + let payload_sha256 = match string_field(payload, "payloadSha256") { + std::option::Option::Some(value) if is_sha256(value) => value.to_string(), + _ => return std::result::Result::Err("Memo payload SHA-256 is invalid".to_string()), + }; + let signer_validation = match payload.get("signerValidation") { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err("Memo signer validation is absent".to_string()); + }, + }; + let signer_validation_status = match string_field(signer_validation, "status") { + std::option::Option::Some(value) if value == generation.signer_status() => { + value.to_string() + }, + _ => { + return std::result::Result::Err( + "Memo signer validation status is inconsistent".to_string(), + ); + }, + }; + let missing_signers = match string_array(signer_validation, "missingRequiredSigners") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(message) => return std::result::Result::Err(message), + }; + if !missing_signers.is_empty() { + return std::result::Result::Err("committed Memo reports missing signers".to_string()); + } + let required_signers = match string_array(signer_validation, "orderedRequiredSigners") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(message) => return std::result::Result::Err(message), + }; + let observed_signers = match string_array(signer_validation, "orderedObservedSigners") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(message) => return std::result::Result::Err(message), + }; + let verified_signers = if generation == MemoGeneration::V1 { + if !required_signers.is_empty() { + return std::result::Result::Err("Memo v1 cannot require signers".to_string()); + } + std::vec::Vec::new() + } else { + if required_signers != observed_signers { + return std::result::Result::Err( + "committed Memo required and observed signers differ".to_string(), + ); + } + observed_signers.clone() + }; + return std::result::Result::Ok(MemoProjection { + generation, + memo_text, + payload_length_bytes, + payload_sha256, + required_signers, + observed_signers, + verified_signers, + signer_validation_status, + }); +} + +fn string_field<'a>(value: &'a serde_json::Value, key: &str) -> std::option::Option<&'a str> { + return value.get(key).and_then(serde_json::Value::as_str); +} + +fn string_array( + value: &serde_json::Value, + key: &str, +) -> std::result::Result, std::string::String> { + let values = match value.get(key).and_then(serde_json::Value::as_array) { + std::option::Option::Some(values) => values, + std::option::Option::None => { + return std::result::Result::Err(format!("Memo signer field {key} is not an array")); + }, + }; + let mut output = std::vec::Vec::with_capacity(values.len()); + for item in values { + let text = match item.as_str() { + std::option::Option::Some(text) if !text.trim().is_empty() => text, + _ => { + return std::result::Result::Err(format!( + "Memo signer field {key} contains an invalid pubkey" + )); + }, + }; + output.push(text.to_string()); + } + return std::result::Result::Ok(output); +} + +fn is_sha256(value: &str) -> bool { + return value.len() == 64 + && value + .as_bytes() + .iter() + .all(|byte| return byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase()); +} + +fn failed(code: &str, message: std::string::String) -> crate::MtApiMaterializerExecutionResult { + return crate::MtApiMaterializerExecutionResult { + status: crate::MtApiMaterializerOutcomeStatus::Failed, + outputs: std::vec::Vec::new(), + diagnostics: std::vec![crate::MtApiMaterializerDiagnostic { + code: code.to_string(), + message, + retriable: false, + }], + }; +} + +#[cfg(test)] +mod tests { + fn observation( + program_id: &str, + surface_code: &str, + generation: &str, + event_name: &str, + failed: bool, + committed: bool, + required_signers: serde_json::Value, + observed_signers: serde_json::Value, + ) -> crate::DcApiDecodedObservation { + return crate::DcApiDecodedObservation { + event_key: "memo:0".to_string(), + event: crate::MdDecodedProtocolEvent { + signature: crate::MdSignature("signature".to_string()), + slot: crate::MdSlot(42), + instruction_path: crate::MdInstructionPath("1/0".to_string()), + program_id: crate::MdProgramId(program_id.to_string()), + protocol_code: crate::MdProtocolCode("spl.memo".to_string()), + surface_code: crate::MdSurfaceCode(surface_code.to_string()), + event_code: crate::MdEventCode(format!("{surface_code}.{event_name}")), + event_name: crate::MdEventName(event_name.to_string()), + event_family: crate::MdEventFamily::Audit, + source_kind: crate::MdEventSourceKind::InnerInstruction, + confidence: crate::MdDecoderConfidence::ManualExact, + }, + payload_json: serde_json::json!({ + "generation": generation, + "programId": program_id, + "instructionPath": "1/0", + "transactionSucceeded": !failed, + "committed": committed, + "memoText": "memo", + "payloadLengthBytes": 4, + "payloadSha256": "1111111111111111111111111111111111111111111111111111111111111111", + "payloadComplete": true, + "utf8Validation": {"status":"valid","invalidFromByte":null}, + "signerValidation": { + "status": if generation == "v1" { "not_applicable" } else { "valid" }, + "orderedRequiredSigners": required_signers, + "orderedObservedSigners": observed_signers, + "missingRequiredSigners": [] + }, + "runtimeValidation": if failed { + "not_proven_transaction_failed" + } else { + "proven_by_successful_transaction" + } + }), + transaction_failed: failed, + transaction_error: if failed { + std::option::Option::Some(serde_json::json!({"InstructionError":[0,"Custom"]})) + } else { + std::option::Option::None + }, + observation_committed: committed, + proof: crate::DcApiDecoderProof { + kind: crate::DcApiDecoderProofKind::Manual, + confidence: crate::MdDecoderConfidence::ManualExact, + evidence: std::vec!["memo".to_string()], + }, + }; + } + + fn v4() -> crate::DcApiDecodedObservation { + return observation( + kb_program_ids::SPL_MEMO_V4_PROGRAM_ID, + "spl.memo.v4", + "v4", + "add_memo", + false, + true, + serde_json::json!(["signer", "signer"]), + serde_json::json!(["signer", "signer"]), + ); + } + + #[test] + fn committed_v4_memo_becomes_deterministic_transaction_annotation() { + let materializer = crate::MtTransactionAnnotationMaterializer; + let first = crate::MtApiEventMaterializer::materialize(&materializer, &v4()); + let second = crate::MtApiEventMaterializer::materialize(&materializer, &v4()); + assert_eq!(first, second); + assert_eq!(first.status, crate::MtApiMaterializerOutcomeStatus::Inserted); + assert_eq!(first.outputs.len(), 1); + assert_eq!( + first.outputs[0].family, + crate::MdMaterializedEventFamily::TransactionAnnotation + ); + assert_eq!( + first.outputs[0].payload_json["signers"]["verified"], + serde_json::json!(["signer", "signer"]) + ); + assert_eq!(first.outputs[0].payload_json["committed"], true); + assert!(first.validate().is_ok()); + } + + #[test] + fn exact_v1_v3_and_v4_surfaces_are_accepted() { + for (program_id, surface, generation) in [ + (kb_program_ids::SPL_MEMO_V1_PROGRAM_ID, "spl.memo.v1", "v1"), + (kb_program_ids::SPL_MEMO_V3_PROGRAM_ID, "spl.memo.v3", "v3"), + (kb_program_ids::SPL_MEMO_V4_PROGRAM_ID, "spl.memo.v4", "v4"), + ] { + let observation = observation( + program_id, + surface, + generation, + "add_memo", + false, + true, + serde_json::json!([]), + serde_json::json!([]), + ); + assert!(crate::MtApiEventMaterializer::accepts_observation( + &crate::MtTransactionAnnotationMaterializer, + &observation, + )); + } + } + + #[test] + fn v1_observed_accounts_are_not_claimed_as_runtime_verified() { + let observation = observation( + kb_program_ids::SPL_MEMO_V1_PROGRAM_ID, + "spl.memo.v1", + "v1", + "add_memo", + false, + true, + serde_json::json!([]), + serde_json::json!(["observed_only"]), + ); + let result = crate::MtApiEventMaterializer::materialize( + &crate::MtTransactionAnnotationMaterializer, + &observation, + ); + assert_eq!(result.outputs[0].payload_json["signers"]["verified"], serde_json::json!([])); + assert_eq!( + result.outputs[0].payload_json["signers"]["observed"], + serde_json::json!(["observed_only"]) + ); + } + + #[test] + fn failed_or_uncommitted_memo_is_refused_without_output() { + let observation = observation( + kb_program_ids::SPL_MEMO_V4_PROGRAM_ID, + "spl.memo.v4", + "v4", + "memo_intent", + true, + false, + serde_json::json!([]), + serde_json::json!([]), + ); + let materializer = crate::MtTransactionAnnotationMaterializer; + let policy = + crate::materializer_api_validate_materialization_policy(&materializer, &observation); + assert!(policy.is_err()); + let result = crate::MtApiEventMaterializer::materialize(&materializer, &observation); + assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Refused); + assert!(result.outputs.is_empty()); + } + + #[test] + fn malformed_committed_payload_fails_closed() { + let mut observation = v4(); + observation.payload_json["payloadSha256"] = serde_json::Value::String("bad".to_string()); + let result = crate::MtApiEventMaterializer::materialize( + &crate::MtTransactionAnnotationMaterializer, + &observation, + ); + assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Failed); + assert!(result.outputs.is_empty()); + assert_eq!(result.diagnostics[0].code, "memo_annotation_payload_invalid"); + } + + #[test] + fn foreign_audit_observation_is_not_accepted() { + let mut observation = v4(); + observation.event.protocol_code = crate::MdProtocolCode("foreign".to_string()); + assert!(!crate::MtApiEventMaterializer::accepts_observation( + &crate::MtTransactionAnnotationMaterializer, + &observation, + )); + let result = crate::MtApiEventMaterializer::materialize( + &crate::MtTransactionAnnotationMaterializer, + &observation, + ); + assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Ignored); + } + + #[test] + fn machine_readable_matrix_declares_transaction_annotation_policy() { + let parsed = serde_json::from_str::(include_str!( + "../../../../../test-fixtures/contract-matrices/SPL_MEMO_MATRIX.json" + )); + let matrix = match parsed { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("memo matrix is invalid JSON: {error}"), + }; + assert_eq!(matrix["matrixVersion"], 4); + assert_eq!(matrix["materializationContract"]["crate"], "kb-lib"); + assert_eq!( + matrix["materializationContract"]["transactionPolicy"], + "SuccessfulCommittedOnly" + ); + assert_eq!(matrix["materializationContract"]["failedOrUncommittedOutput"], false); + } +} diff --git a/kb-lib/src/materializer/transaction/constants.rs b/kb-lib/src/materializer/transaction/constants.rs deleted file mode 100644 index 61c643e..0000000 --- a/kb-lib/src/materializer/transaction/constants.rs +++ /dev/null @@ -1,15 +0,0 @@ -// file: kb-lib/src/materializer/transaction/constants.rs -// version: 3 - -//! Local constants for the transaction annotation materializer. - -/// Maximum Memo payload accepted from the decoded contract. -pub(crate) const MT_TRANSACTION_ANNOTATIONS_MAX_MEMO_PAYLOAD_BYTES: u64 = 4_096; -/// Stable materializer processor name. -pub(crate) const MT_TRANSACTION_ANNOTATIONS_PROCESSOR_NAME: &str = - "materializer.transaction.annotations"; -/// Stable projection contract version. -pub(crate) const MT_TRANSACTION_ANNOTATIONS_PROJECTION_VERSION: u32 = 1; -/// Canonical tracing target for this component. -pub(crate) const TRACING_TARGET_MATERIALIZER_TRANSACTION_ANNOTATIONS: &str = - "kb-lib.materializer.transaction"; diff --git a/kb-lib/src/materializer/vault.rs b/kb-lib/src/materializer/vault.rs index 8955367..4630cd7 100644 --- a/kb-lib/src/materializer/vault.rs +++ b/kb-lib/src/materializer/vault.rs @@ -1,9 +1,17 @@ // file: kb-lib/src/materializer/vault.rs -// version: 3 +// version: 4 //! `vault` materializer family. -mod core; +mod constants; +mod materializer; /// Reserved vault materializer. -pub use self::core::MtVaultMaterializer; +pub use self::materializer::MtVaultMaterializer; + +/// Empty accepted-family boundary for the reserved component. +pub(crate) use self::constants::MT_VAULT_ACCEPTED_FAMILIES; +/// Stable runtime component name for the `vault` materializer. +pub(crate) use self::constants::MT_VAULT_COMPONENT_NAME; +/// Stable processor name for the `vault` materializer. +pub(crate) use self::constants::MT_VAULT_PROCESSOR_NAME; diff --git a/kb-lib/src/materializer/vault/constants.rs b/kb-lib/src/materializer/vault/constants.rs new file mode 100644 index 0000000..44dbbb7 --- /dev/null +++ b/kb-lib/src/materializer/vault/constants.rs @@ -0,0 +1,11 @@ +// file: kb-lib/src/materializer/vault/constants.rs +// version: 1 + +//! Local constants for the vault materializer. + +/// Stable runtime component name for the vault materializer. +pub(crate) const MT_VAULT_COMPONENT_NAME: &str = "kb-lib.materializer.vault"; +/// Stable processor name persisted by the replay ledger. +pub(crate) const MT_VAULT_PROCESSOR_NAME: &str = "materializer.vault"; +/// Empty family boundary retained while the materializer is reserved. +pub(crate) const MT_VAULT_ACCEPTED_FAMILIES: &[crate::MdEventFamily] = &[]; diff --git a/kb-lib/src/materializer/vault/core.rs b/kb-lib/src/materializer/vault/materializer.rs similarity index 72% rename from kb-lib/src/materializer/vault/core.rs rename to kb-lib/src/materializer/vault/materializer.rs index a184166..05421be 100644 --- a/kb-lib/src/materializer/vault/core.rs +++ b/kb-lib/src/materializer/vault/materializer.rs @@ -1,5 +1,5 @@ -// file: kb-lib/src/materializer/vault/core.rs -// version: 4 +// file: kb-lib/src/materializer/vault/materializer.rs +// version: 5 //! Reserved vault materializer. @@ -9,7 +9,7 @@ pub struct MtVaultMaterializer; impl crate::MtMaterializer for crate::MtVaultMaterializer { fn materializer_name(&self) -> &'static str { - return "kb-lib.materializer.vault"; + return crate::MT_VAULT_COMPONENT_NAME; } fn materializer_version(&self) -> &'static str { @@ -22,27 +22,22 @@ impl crate::MtMaterializer for crate::MtVaultMaterializer { fn materialize_event( &self, - event: &crate::MdDecodedProtocolEvent, + _event: &crate::MdDecodedProtocolEvent, ) -> kb_core::Result> { - return std::result::Result::Ok(std::vec![crate::MdMaterializedEvent { - signature: event.signature.clone(), - slot: event.slot, - decoded_family: event.event_family, - materialized_family: crate::MdMaterializedEventFamily::Vault, - }]); + return std::result::Result::Ok(std::vec::Vec::new()); } } impl crate::MtApiEventMaterializer for crate::MtVaultMaterializer { fn identity(&self) -> crate::MtApiMaterializerIdentity { return crate::MtApiMaterializerIdentity { - name: "kb-lib.materializer.vault".to_string(), + name: crate::MT_VAULT_PROCESSOR_NAME.to_string(), version: env!("CARGO_PKG_VERSION").to_string(), }; } fn accepted_families(&self) -> &'static [crate::MdEventFamily] { - return &[]; + return crate::MT_VAULT_ACCEPTED_FAMILIES; } fn transaction_policy( diff --git a/kb-lib/tests/external_materializer_api.rs b/kb-lib/tests/external_materializer_api.rs index 5c4926f..c535e36 100644 --- a/kb-lib/tests/external_materializer_api.rs +++ b/kb-lib/tests/external_materializer_api.rs @@ -1,5 +1,5 @@ // file: kb-lib/tests/external_materializer_api.rs -// version: 6 +// version: 7 //! Downstream-style compilation contract for externally implemented materializers. @@ -14,7 +14,7 @@ fn assert_legacy_materializer() {} impl kb_lib::MtApiEventMaterializer for ExternalAuditMaterializer { fn identity(&self) -> kb_lib::MtApiMaterializerIdentity { return kb_lib::MtApiMaterializerIdentity { - name: "external_audit_materializer".to_string(), + name: "materializer.external.audit".to_string(), version: "1".to_string(), }; } @@ -41,7 +41,7 @@ impl kb_lib::MtApiEventMaterializer for ExternalAuditMaterializer { #[test] fn external_materializer_uses_only_the_public_kb_lib_contract() { let materializer: &dyn kb_lib::MtApiEventMaterializer = &ExternalAuditMaterializer; - assert_eq!(materializer.identity().name, "external_audit_materializer"); + assert_eq!(materializer.identity().name, "materializer.external.audit"); assert_eq!(materializer.accepted_families(), ACCEPTED_FAMILIES); assert_event_materializer::(); assert_event_materializer::(); @@ -51,6 +51,7 @@ fn external_materializer_uses_only_the_public_kb_lib_contract() { assert_event_materializer::(); assert_event_materializer::(); assert_event_materializer::(); + assert_event_materializer::(); assert_event_materializer::(); assert_event_materializer::(); assert_event_materializer::(); @@ -71,6 +72,7 @@ fn external_materializer_uses_only_the_public_kb_lib_contract() { assert_legacy_materializer::(); assert_legacy_materializer::(); assert_legacy_materializer::(); + assert_legacy_materializer::(); assert_legacy_materializer::(); assert_legacy_materializer::(); assert_legacy_materializer::(); diff --git a/kb-lib/tests/external_solana_program_metadata_executor_api.rs b/kb-lib/tests/external_solana_program_metadata_executor_api.rs new file mode 100644 index 0000000..871bf1b --- /dev/null +++ b/kb-lib/tests/external_solana_program_metadata_executor_api.rs @@ -0,0 +1,63 @@ +// file: kb-lib/tests/external_solana_program_metadata_executor_api.rs +// version: 1 + +//! Downstream-style compilation contract for the Solana Program Metadata executor. + +#[test] +fn external_consumers_build_solana_program_metadata_plans_only_through_crate_root_exports() { + let authority = kb_lib::MdPubkey(kb_program_ids::SYSTEM_PROGRAM_ID.to_string()); + let account = kb_lib::MdPubkey(kb_program_ids::VOTE_PROGRAM_ID.to_string()); + let policy = kb_lib::ExApiExecutionPolicy { + cost_limit: kb_lib::ExApiExecutionCostLimit { + max_spend_lamports: std::option::Option::Some(0), + max_fee_lamports: std::option::Option::Some(20_000), + max_compute_unit_price_micro_lamports: std::option::Option::None, + }, + authorized_signers: std::vec![authority.clone()], + post_execution_validation: kb_lib::ExApiPostExecutionValidationPolicy { + canonical_insert_required: true, + core_extraction_required: true, + decode_replay_required: true, + materialization_required: true, + }, + ..kb_lib::ExApiExecutionPolicy::default() + }; + let intent = kb_lib::ExMetadataSpmExecutionIntent { + intent_id: "external-spm-extend".to_string(), + fee_payer: authority.clone(), + policy, + allow_destructive_operation: false, + operation: kb_lib::ExMetadataSpmOperation::Extend { + account, + authority, + length: 64, + program_context: std::option::Option::None, + }, + }; + let executor = kb_lib::ExMetadataSolanaProgramMetadataExecutor; + let program_id = kb_lib::MdProgramId( + kb_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID.to_string(), + ); + let capability = kb_lib::ExApiTypedInstructionExecutor::capability( + &executor, + &program_id, + kb_lib::EX_METADATA_SPM_EXTEND_OPERATION, + ); + assert!(capability.is_supported()); + let plan = kb_lib::ExApiTypedInstructionExecutor::build_prepared_plan(&executor, &intent); + assert!(plan.is_ok()); + let plan = match plan { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + assert_eq!(plan.operation_code, kb_lib::EX_METADATA_SPM_EXTEND_OPERATION); + assert_eq!(plan.instructions.len(), 1); + assert_eq!(plan.instructions[0].data, std::vec![8, 64, 0]); + assert_eq!(plan.instructions[0].accounts.len(), 4); + let evaluation = kb_lib::ExSafetyChecker.evaluate_prepared_plan(&plan); + assert!(evaluation.is_ok()); + assert_eq!( + evaluation.as_ref().map(|value| return value.decision), + std::result::Result::Ok(kb_lib::ExSafetyDecision::Allow) + ); +} diff --git a/scripts/audit_khadhroony_workspace_rules.py b/scripts/audit_khadhroony_workspace_rules.py index 7c4d926..f12020e 100755 --- a/scripts/audit_khadhroony_workspace_rules.py +++ b/scripts/audit_khadhroony_workspace_rules.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # file: scripts/audit_khadhroony_workspace_rules.py -# version: 14 +# version: 17 """Audit mechanically verifiable rules specific to khadhroony-bot3.""" @@ -8,6 +8,7 @@ from __future__ import annotations import argparse import dataclasses +import json import pathlib import re import sys @@ -100,29 +101,31 @@ def audit_kb_lib_tracing(root: pathlib.Path) -> list[Violation]: if match.group(1).startswith("TRACING_TARGET_"): re_exports.add(match.group(1)) target_pattern = re.compile( - r'^pub\(crate\) const (TRACING_TARGET_[A-Z0-9_]+): &str = "([^"]+)";$', + r'pub\(crate\) const (TRACING_TARGET_[A-Z0-9_]+): &str\s*=\s*"([^"]+)";', re.MULTILINE, ) + declared_targets: dict[str, tuple[str, int]] = {} for constants in sorted((crate / "src").rglob("constants.rs")): text = constants.read_text(encoding="utf-8") - match = target_pattern.search(text) - if match is None: - continue component = constants.parent.relative_to(crate / "src") expected = "kb-lib." + ".".join(component.parts) relative = constants.relative_to(root).as_posix() - if match.group(2) != expected: - violations.append( - Violation( - "KH_TRACE101", - relative, - text[: match.start()].count("\n") + 1, - f"`{match.group(1)}` must use hierarchical target `{expected}`", + for match in target_pattern.finditer(text): + line = text[: match.start()].count("\n") + 1 + declared_targets[match.group(1)] = (relative, line) + if match.group(2) != expected: + violations.append( + Violation( + "KH_TRACE101", + relative, + line, + f"`{match.group(1)}` must use hierarchical target `{expected}`", + ) ) - ) macro_pattern = re.compile( r"tracing::(?:debug|error|info|trace|warn)!\(\s*target:\s*crate::(TRACING_TARGET_[A-Z0-9_]+)" ) + used_targets: set[str] = set() for path in sorted((crate / "src").rglob("*.rs")): relative = path.relative_to(root).as_posix() text = path.read_text(encoding="utf-8") @@ -137,6 +140,7 @@ def audit_kb_lib_tracing(root: pathlib.Path) -> list[Violation]: ) ) for match in macro_pattern.finditer(text): + used_targets.add(match.group(1)) if match.group(1) not in re_exports: violations.append( Violation( @@ -146,6 +150,16 @@ def audit_kb_lib_tracing(root: pathlib.Path) -> list[Violation]: f"missing crate-root re-export for `{match.group(1)}`", ) ) + for target, (relative, line) in sorted(declared_targets.items()): + if target not in used_targets: + violations.append( + Violation( + "KH_TRACE104", + relative, + line, + f"declared operational tracing target `{target}` is not used by a real tracing event", + ) + ) return violations @@ -358,6 +372,326 @@ def audit_reserved_materializer_scaffolds(root: pathlib.Path) -> list[Violation] "reserved materializer must return an ignored API result", ) ) + if "std::result::Result::Ok(std::vec::Vec::new())" not in text: + violations.append( + Violation( + "KH_MT007", + relative, + 1, + "reserved materializer legacy contract must return an empty event list", + ) + ) + if not re.search(r"return crate::MT_[A-Z0-9_]+_ACCEPTED_FAMILIES;", text): + violations.append( + Violation( + "KH_MT008", + relative, + 1, + "reserved materializer must use its exported accepted-family constant", + ) + ) + return violations + + +def iter_rust_json_object_macros(text: str) -> list[tuple[int, str]]: + """Return balanced `serde_json::json!({ ... })` object macro bodies.""" + + blocks: list[tuple[int, str]] = [] + needle = "serde_json::json!({" + offset = 0 + while True: + start = text.find(needle, offset) + if start < 0: + break + brace = text.find("{", start) + depth = 0 + in_string = False + escaped = False + end = brace + while end < len(text): + character = text[end] + if in_string: + if escaped: + escaped = False + elif character == "\\": + escaped = True + elif character == '"': + in_string = False + elif character == '"': + in_string = True + elif character == "{": + depth += 1 + elif character == "}": + depth -= 1 + if depth == 0: + blocks.append((start, text[start: end + 1])) + offset = end + 1 + break + end += 1 + else: + offset = start + len(needle) + return blocks + + +def audit_materializer_conventions(root: pathlib.Path) -> list[Violation]: + """Audit materializer names, constants and component facade structure.""" + + violations: list[Violation] = [] + materializer_root = root / "kb-lib/src/materializer" + component_pattern = re.compile( + r'pub\(crate\) const (MT_[A-Z0-9_]+)_COMPONENT_NAME: &str\s*=\s*"([^"]+)";' + ) + processor_pattern = re.compile( + r'pub\(crate\) const (MT_[A-Z0-9_]+)_PROCESSOR_NAME: &str\s*=\s*"([^"]+)";' + ) + component_names: dict[str, tuple[str, str, int]] = {} + processor_names: dict[str, tuple[str, str, int]] = {} + for path in sorted(materializer_root.rglob("constants.rs")): + relative = path.relative_to(root).as_posix() + text = path.read_text(encoding="utf-8") + for match in component_pattern.finditer(text): + prefix = match.group(1) + line = text[: match.start()].count("\n") + 1 + component_names[prefix] = (match.group(2), relative, line) + for match in processor_pattern.finditer(text): + prefix = match.group(1) + line = text[: match.start()].count("\n") + 1 + processor_names[prefix] = (match.group(2), relative, line) + for prefix, (component_name, relative, line) in sorted(component_names.items()): + processor = processor_names.get(prefix) + if processor is None: + violations.append( + Violation( + "KH_MT009", + relative, + line, + f"`{prefix}` component name requires one matching processor name", + ) + ) + continue + processor_name = processor[0] + if not component_name.startswith("kb-lib.materializer."): + violations.append( + Violation( + "KH_MT010", + relative, + line, + f"materializer component name `{component_name}` must start with `kb-lib.materializer.`", + ) + ) + expected_processor = component_name.removeprefix("kb-lib.") + if processor_name != expected_processor: + violations.append( + Violation( + "KH_MT011", + processor[1], + processor[2], + f"processor name `{processor_name}` must equal `{expected_processor}`", + ) + ) + for prefix, (_, relative, line) in sorted(processor_names.items()): + if prefix not in component_names: + violations.append( + Violation( + "KH_MT012", + relative, + line, + f"`{prefix}` processor name requires one matching component name", + ) + ) + for path in sorted(materializer_root.rglob("constants.rs")): + relative = path.relative_to(root).as_posix() + text = path.read_text(encoding="utf-8") + for match in component_pattern.finditer(text): + symbol = f"{match.group(1)}_COMPONENT_NAME" + used_by_implementation = any( + symbol in candidate.read_text(encoding="utf-8") + for candidate in path.parent.rglob("*.rs") + if candidate.name != "constants.rs" + ) + if not used_by_implementation: + violations.append( + Violation( + "KH_MT025", + relative, + text[: match.start()].count("\n") + 1, + f"materializer component constant `{symbol}` is not consumed by its implementation", + ) + ) + component_values = {value[0] for value in component_names.values()} + processor_values = {value[0] for value in processor_names.values()} + if len(component_values) != len(component_names): + violations.append( + Violation( + "KH_MT017", + "kb-lib/src/materializer", + 1, + "materializer component names must be unique", + ) + ) + if len(processor_values) != len(processor_names): + violations.append( + Violation( + "KH_MT018", + "kb-lib/src/materializer", + 1, + "materializer processor names must be unique", + ) + ) + tracing_pattern = re.compile( + r'pub\(crate\) const TRACING_TARGET_MATERIALIZER_[A-Z0-9_]+: &str\s*=\s*"([^"]+)";' + ) + for path in sorted(materializer_root.rglob("constants.rs")): + relative = path.relative_to(root).as_posix() + text = path.read_text(encoding="utf-8") + for match in tracing_pattern.finditer(text): + if match.group(1) not in component_values: + line = text[: match.start()].count("\n") + 1 + violations.append( + Violation( + "KH_MT019", + relative, + line, + f"materializer tracing target `{match.group(1)}` must equal one component name", + ) + ) + config_path = root / "config/example.config.json" + if config_path.exists(): + try: + config = json.loads(config_path.read_text(encoding="utf-8")) + except json.JSONDecodeError: + config = None + pending: list[object] = [config] if config is not None else [] + while pending: + value = pending.pop() + if isinstance(value, dict): + targets = value.get("targets") + if isinstance(targets, list): + for target in targets: + if ( + isinstance(target, str) + and target.startswith("kb-lib.materializer.") + and target not in component_values + ): + violations.append( + Violation( + "KH_MT020", + "config/example.config.json", + 1, + f"logging target `{target}` is not a declared materializer component", + ) + ) + pending.extend(value.values()) + elif isinstance(value, list): + pending.extend(value) + for path in sorted(materializer_root.rglob("*.rs")): + relative = path.relative_to(root).as_posix() + text = path.read_text(encoding="utf-8") + if path.name != "constants.rs" and re.search(r'"processorName"\s*:\s*"', text): + violations.append( + Violation( + "KH_MT021", + relative, + 1, + "persisted processor names must come from exported component constants", + ) + ) + for offset, block in iter_rust_json_object_macros(text): + if '"projectionVersion"' not in block: + continue + line = text[:offset].count("\n") + 1 + if '"processorName"' not in block: + violations.append( + Violation( + "KH_MT022", + relative, + line, + "versioned materializer output must persist its processor provenance", + ) + ) + if re.search( + r'"projectionVersion"\s*:\s*crate::MT_[A-Z0-9_]+_PROJECTION_VERSION', + block, + ) is None: + violations.append( + Violation( + "KH_MT023", + relative, + line, + "materializer projection version must come from an exported component constant", + ) + ) + if re.search( + r'"processorName"\s*:\s*crate::MT_[A-Z0-9_]+_PROCESSOR_NAME', + block, + ) is None: + violations.append( + Violation( + "KH_MT024", + relative, + line, + "materializer processor provenance must come from an exported component constant", + ) + ) + if path.name != "constants.rs" and re.search( + r"^\s*(?:pub(?:\(crate\))?\s+)?const\s+[A-Z][A-Z0-9_]*\b", + text, + re.MULTILINE, + ): + violations.append( + Violation( + "KH_MT013", + relative, + 1, + "materializer constants must be declared in the component `constants.rs`", + ) + ) + if path.name == "materializer.rs" and re.search( + r"^pub struct Mt[A-Za-z0-9]+Materializer;", text, re.MULTILINE + ): + constants_path = path.parent / "constants.rs" + if not constants_path.exists(): + violations.append( + Violation( + "KH_MT014", + relative, + 1, + "materializer implementation requires a sibling `constants.rs`", + ) + ) + if re.search(r'"(?:kb-lib\.)?materializer\.[^"]+"', text): + violations.append( + Violation( + "KH_MT015", + relative, + 1, + "materializer identities must come from exported component constants", + ) + ) + module_lines = [ + line + for line in text.splitlines() + if re.fullmatch(r"mod [A-Za-z_][A-Za-z0-9_]*;", line.strip()) is not None + ] + if not module_lines: + continue + for index, line in enumerate(text.splitlines(), 1): + stripped = line.strip() + if not stripped or stripped.startswith("//") or stripped == "#[cfg(test)]": + continue + if re.fullmatch(r"mod [A-Za-z_][A-Za-z0-9_]*;", stripped) is not None: + continue + if stripped.startswith("pub use ") or stripped.startswith("pub(crate) use "): + continue + violations.append( + Violation( + "KH_MT016", + relative, + index, + "materializer facade may contain only submodule declarations and re-exports", + ) + ) + break return violations @@ -440,13 +774,13 @@ def audit_reserved_executor_scaffolds(root: pathlib.Path) -> list[Violation]: "reserved executor must build only an explicit zero-instruction plan", ) ) - if reserved_count != 104: + if reserved_count != 103: violations.append( Violation( "KH_EX007", "kb-lib/src/executor", 1, - f"expected 104 reserved executor boundaries, found {reserved_count}", + f"expected 103 reserved executor boundaries, found {reserved_count}", ) ) return violations @@ -596,15 +930,16 @@ def main() -> int: root = pathlib.Path(arguments.root).resolve() violations = ( audit_tracing(root) - + audit_kb_lib_tracing(root) - + audit_kb_lib_symbol_prefixes(root) - + audit_reserved_decoder_scaffolds(root) - + audit_reserved_materializer_scaffolds(root) - + audit_reserved_executor_scaffolds(root) - + audit_token_2022_naming(root) - + audit_private_kb_lib_paths_in_active_docs(root) - + audit_solana_types(root) - + audit_wincode_resolution(root) + +audit_kb_lib_tracing(root) + +audit_kb_lib_symbol_prefixes(root) + +audit_reserved_decoder_scaffolds(root) + +audit_reserved_materializer_scaffolds(root) + +audit_materializer_conventions(root) + +audit_reserved_executor_scaffolds(root) + +audit_token_2022_naming(root) + +audit_private_kb_lib_paths_in_active_docs(root) + +audit_solana_types(root) + +audit_wincode_resolution(root) ) violations.sort(key=lambda item: (item.code, item.path, item.line, item.message)) if not violations: diff --git a/test-fixtures/contract-matrices/OPERATION_NAMING_MATRIX.json b/test-fixtures/contract-matrices/OPERATION_NAMING_MATRIX.json index 086adcb..7837b23 100644 --- a/test-fixtures/contract-matrices/OPERATION_NAMING_MATRIX.json +++ b/test-fixtures/contract-matrices/OPERATION_NAMING_MATRIX.json @@ -2560,6 +2560,105 @@ "decoderName": "kb-lib.decoder.spl.token_2022", "status": "implemented", "source": "kb-lib/src/executor/spl/token_2022/intent.rs" + }, + { + "programId": "ProgM6JCCvbYkfKqJYHePx4xxSUSqJp7rh8Lyv7nk7S", + "processorName": "metadata.solana_program_metadata", + "surfaceCode": "metadata.solana_program_metadata", + "operationCode": "metadata.solana_program_metadata.allocate", + "eventCode": "metadata.solana_program_metadata.allocate", + "executorName": "kb-lib.executor.metadata.solana_program_metadata", + "decoderName": "kb-lib.decoder.metadata.solana_program_metadata", + "status": "implemented", + "source": "kb-lib/src/executor/metadata/solana_program_metadata/constants.rs" + }, + { + "programId": "ProgM6JCCvbYkfKqJYHePx4xxSUSqJp7rh8Lyv7nk7S", + "processorName": "metadata.solana_program_metadata", + "surfaceCode": "metadata.solana_program_metadata", + "operationCode": "metadata.solana_program_metadata.close", + "eventCode": "metadata.solana_program_metadata.close", + "executorName": "kb-lib.executor.metadata.solana_program_metadata", + "decoderName": "kb-lib.decoder.metadata.solana_program_metadata", + "status": "implemented", + "source": "kb-lib/src/executor/metadata/solana_program_metadata/constants.rs" + }, + { + "programId": "ProgM6JCCvbYkfKqJYHePx4xxSUSqJp7rh8Lyv7nk7S", + "processorName": "metadata.solana_program_metadata", + "surfaceCode": "metadata.solana_program_metadata", + "operationCode": "metadata.solana_program_metadata.extend", + "eventCode": "metadata.solana_program_metadata.extend", + "executorName": "kb-lib.executor.metadata.solana_program_metadata", + "decoderName": "kb-lib.decoder.metadata.solana_program_metadata", + "status": "implemented", + "source": "kb-lib/src/executor/metadata/solana_program_metadata/constants.rs" + }, + { + "programId": "ProgM6JCCvbYkfKqJYHePx4xxSUSqJp7rh8Lyv7nk7S", + "processorName": "metadata.solana_program_metadata", + "surfaceCode": "metadata.solana_program_metadata", + "operationCode": "metadata.solana_program_metadata.initialize", + "eventCode": "metadata.solana_program_metadata.initialize", + "executorName": "kb-lib.executor.metadata.solana_program_metadata", + "decoderName": "kb-lib.decoder.metadata.solana_program_metadata", + "status": "implemented", + "source": "kb-lib/src/executor/metadata/solana_program_metadata/constants.rs" + }, + { + "programId": "ProgM6JCCvbYkfKqJYHePx4xxSUSqJp7rh8Lyv7nk7S", + "processorName": "metadata.solana_program_metadata", + "surfaceCode": "metadata.solana_program_metadata", + "operationCode": "metadata.solana_program_metadata.set_authority", + "eventCode": "metadata.solana_program_metadata.set_authority", + "executorName": "kb-lib.executor.metadata.solana_program_metadata", + "decoderName": "kb-lib.decoder.metadata.solana_program_metadata", + "status": "implemented", + "source": "kb-lib/src/executor/metadata/solana_program_metadata/constants.rs" + }, + { + "programId": "ProgM6JCCvbYkfKqJYHePx4xxSUSqJp7rh8Lyv7nk7S", + "processorName": "metadata.solana_program_metadata", + "surfaceCode": "metadata.solana_program_metadata", + "operationCode": "metadata.solana_program_metadata.set_data", + "eventCode": "metadata.solana_program_metadata.set_data", + "executorName": "kb-lib.executor.metadata.solana_program_metadata", + "decoderName": "kb-lib.decoder.metadata.solana_program_metadata", + "status": "implemented", + "source": "kb-lib/src/executor/metadata/solana_program_metadata/constants.rs" + }, + { + "programId": "ProgM6JCCvbYkfKqJYHePx4xxSUSqJp7rh8Lyv7nk7S", + "processorName": "metadata.solana_program_metadata", + "surfaceCode": "metadata.solana_program_metadata", + "operationCode": "metadata.solana_program_metadata.set_immutable", + "eventCode": "metadata.solana_program_metadata.set_immutable", + "executorName": "kb-lib.executor.metadata.solana_program_metadata", + "decoderName": "kb-lib.decoder.metadata.solana_program_metadata", + "status": "implemented", + "source": "kb-lib/src/executor/metadata/solana_program_metadata/constants.rs" + }, + { + "programId": "ProgM6JCCvbYkfKqJYHePx4xxSUSqJp7rh8Lyv7nk7S", + "processorName": "metadata.solana_program_metadata", + "surfaceCode": "metadata.solana_program_metadata", + "operationCode": "metadata.solana_program_metadata.trim", + "eventCode": "metadata.solana_program_metadata.trim", + "executorName": "kb-lib.executor.metadata.solana_program_metadata", + "decoderName": "kb-lib.decoder.metadata.solana_program_metadata", + "status": "implemented", + "source": "kb-lib/src/executor/metadata/solana_program_metadata/constants.rs" + }, + { + "programId": "ProgM6JCCvbYkfKqJYHePx4xxSUSqJp7rh8Lyv7nk7S", + "processorName": "metadata.solana_program_metadata", + "surfaceCode": "metadata.solana_program_metadata", + "operationCode": "metadata.solana_program_metadata.write", + "eventCode": "metadata.solana_program_metadata.write", + "executorName": "kb-lib.executor.metadata.solana_program_metadata", + "decoderName": "kb-lib.decoder.metadata.solana_program_metadata", + "status": "implemented", + "source": "kb-lib/src/executor/metadata/solana_program_metadata/constants.rs" } ] } diff --git a/test-fixtures/contract-matrices/SOLANA_PROGRAM_METADATA_EXECUTOR_MATRIX.json b/test-fixtures/contract-matrices/SOLANA_PROGRAM_METADATA_EXECUTOR_MATRIX.json new file mode 100644 index 0000000..d88b7b3 --- /dev/null +++ b/test-fixtures/contract-matrices/SOLANA_PROGRAM_METADATA_EXECUTOR_MATRIX.json @@ -0,0 +1,119 @@ +{ + "file": "test-fixtures/contract-matrices/SOLANA_PROGRAM_METADATA_EXECUTOR_MATRIX.json", + "version": 1, + "programId": "ProgM6JCCvbYkfKqJYHePx4xxSUSqJp7rh8Lyv7nk7S", + "surface": "metadata.solana_program_metadata", + "executor": "kb-lib.executor.metadata.solana_program_metadata", + "operations": [ + { + "tag": 0, + "entryCode": "write", + "operationCode": "metadata.solana_program_metadata.write", + "accountCount": 3, + "current": true, + "deprecated": false, + "replaced": false, + "requiresExplicitApproval": true, + "safetyCheckerRequired": true + }, + { + "tag": 1, + "entryCode": "initialize", + "operationCode": "metadata.solana_program_metadata.initialize", + "accountCount": 5, + "current": true, + "deprecated": false, + "replaced": false, + "requiresExplicitApproval": false, + "safetyCheckerRequired": true + }, + { + "tag": 2, + "entryCode": "set_authority", + "operationCode": "metadata.solana_program_metadata.set_authority", + "accountCount": 4, + "current": true, + "deprecated": false, + "replaced": false, + "requiresExplicitApproval": true, + "safetyCheckerRequired": true + }, + { + "tag": 3, + "entryCode": "set_data", + "operationCode": "metadata.solana_program_metadata.set_data", + "accountCount": 5, + "current": true, + "deprecated": false, + "replaced": false, + "requiresExplicitApproval": true, + "safetyCheckerRequired": true + }, + { + "tag": 4, + "entryCode": "set_immutable", + "operationCode": "metadata.solana_program_metadata.set_immutable", + "accountCount": 4, + "current": true, + "deprecated": false, + "replaced": false, + "requiresExplicitApproval": true, + "safetyCheckerRequired": true + }, + { + "tag": 5, + "entryCode": "trim", + "operationCode": "metadata.solana_program_metadata.trim", + "accountCount": 6, + "current": true, + "deprecated": false, + "replaced": false, + "requiresExplicitApproval": true, + "safetyCheckerRequired": true, + "preStableAlias": "withdraw_excess_lamports" + }, + { + "tag": 6, + "entryCode": "close", + "operationCode": "metadata.solana_program_metadata.close", + "accountCount": 5, + "current": true, + "deprecated": false, + "replaced": false, + "requiresExplicitApproval": true, + "safetyCheckerRequired": true + }, + { + "tag": 7, + "entryCode": "allocate", + "operationCode": "metadata.solana_program_metadata.allocate", + "accountCount": 5, + "current": true, + "deprecated": false, + "replaced": false, + "requiresExplicitApproval": false, + "safetyCheckerRequired": true + }, + { + "tag": 8, + "entryCode": "extend", + "operationCode": "metadata.solana_program_metadata.extend", + "accountCount": 4, + "current": true, + "deprecated": false, + "replaced": false, + "requiresExplicitApproval": false, + "safetyCheckerRequired": true + } + ], + "simulationRequired": true, + "postExecutionValidationRequired": [ + "canonical_insert", + "core_extraction", + "decode_replay", + "materialization" + ], + "offchainContentFetched": false, + "canonicitySelection": "explicit_intent_flag_not_inferred_from_program_data_presence", + "maxInlineContentBytes": 1024 +}