v0.1.0-pre.022

This commit is contained in:
2026-07-24 23:24:14 +02:00
parent 34f96582bc
commit 929c89cf9f
15 changed files with 1450 additions and 69 deletions

View File

@@ -29,7 +29,7 @@
"kb-lib.decoder.spl.memo", "kb-lib.decoder.spl.memo",
"kb-lib.decoder.spl.token", "kb-lib.decoder.spl.token",
"kb-lib.decoder.spl.token2022", "kb-lib.decoder.spl.token2022",
"kb_execution_solana", "kb-lib.executor.solana.transaction",
"kb_executor_metadata_metaplex_token_metadata", "kb_executor_metadata_metaplex_token_metadata",
"kb_executor_metadata_spl_name_service", "kb_executor_metadata_spl_name_service",
"kb-lib.executor.solana.core", "kb-lib.executor.solana.core",
@@ -420,42 +420,42 @@
] ]
}, },
{ {
"name": "file_kb_execution_solana_debug", "name": "file_kb_lib_executor_solana_transaction_debug",
"enabled": true, "enabled": true,
"sink": "file", "sink": "file",
"level": "debug", "level": "debug",
"path": "logs/devnet/kb_execution_solana/debug.log", "path": "logs/devnet/kb-lib/executor/solana/transaction/debug.log",
"rotation": "daily", "rotation": "daily",
"format": "human", "format": "human",
"ansi": false, "ansi": false,
"targets": [ "targets": [
"kb_execution_solana" "kb-lib.executor.solana.transaction"
] ]
}, },
{ {
"name": "file_kb_execution_solana_info", "name": "file_kb_lib_executor_solana_transaction_info",
"enabled": true, "enabled": true,
"sink": "file", "sink": "file",
"level": "info", "level": "info",
"path": "logs/devnet/kb_execution_solana/info.log", "path": "logs/devnet/kb-lib/executor/solana/transaction/info.log",
"rotation": "daily", "rotation": "daily",
"format": "human", "format": "human",
"ansi": false, "ansi": false,
"targets": [ "targets": [
"kb_execution_solana" "kb-lib.executor.solana.transaction"
] ]
}, },
{ {
"name": "file_kb_execution_solana_error", "name": "file_kb_lib_executor_solana_transaction_error",
"enabled": true, "enabled": true,
"sink": "file", "sink": "file",
"level": "error", "level": "error",
"path": "logs/devnet/kb_execution_solana/error.jsonl", "path": "logs/devnet/kb-lib/executor/solana/transaction/error.jsonl",
"rotation": "daily", "rotation": "daily",
"format": "json", "format": "json",
"ansi": false, "ansi": false,
"targets": [ "targets": [
"kb_execution_solana" "kb-lib.executor.solana.transaction"
] ]
}, },
{ {
@@ -1670,7 +1670,7 @@
"kb-lib.decoder.spl.memo", "kb-lib.decoder.spl.memo",
"kb-lib.decoder.spl.token", "kb-lib.decoder.spl.token",
"kb-lib.decoder.spl.token2022", "kb-lib.decoder.spl.token2022",
"kb_execution_solana", "kb-lib.executor.solana.transaction",
"kb_executor_metadata_metaplex_token_metadata", "kb_executor_metadata_metaplex_token_metadata",
"kb_executor_metadata_spl_name_service", "kb_executor_metadata_spl_name_service",
"kb-lib.executor.solana.core", "kb-lib.executor.solana.core",
@@ -2061,42 +2061,42 @@
] ]
}, },
{ {
"name": "file_kb_execution_solana_debug", "name": "file_kb_lib_executor_solana_transaction_debug",
"enabled": true, "enabled": true,
"sink": "file", "sink": "file",
"level": "debug", "level": "debug",
"path": "logs/mainnet_research/kb_execution_solana/debug.log", "path": "logs/mainnet_research/kb-lib/executor/solana/transaction/debug.log",
"rotation": "daily", "rotation": "daily",
"format": "human", "format": "human",
"ansi": false, "ansi": false,
"targets": [ "targets": [
"kb_execution_solana" "kb-lib.executor.solana.transaction"
] ]
}, },
{ {
"name": "file_kb_execution_solana_info", "name": "file_kb_lib_executor_solana_transaction_info",
"enabled": true, "enabled": true,
"sink": "file", "sink": "file",
"level": "info", "level": "info",
"path": "logs/mainnet_research/kb_execution_solana/info.log", "path": "logs/mainnet_research/kb-lib/executor/solana/transaction/info.log",
"rotation": "daily", "rotation": "daily",
"format": "human", "format": "human",
"ansi": false, "ansi": false,
"targets": [ "targets": [
"kb_execution_solana" "kb-lib.executor.solana.transaction"
] ]
}, },
{ {
"name": "file_kb_execution_solana_error", "name": "file_kb_lib_executor_solana_transaction_error",
"enabled": true, "enabled": true,
"sink": "file", "sink": "file",
"level": "error", "level": "error",
"path": "logs/mainnet_research/kb_execution_solana/error.jsonl", "path": "logs/mainnet_research/kb-lib/executor/solana/transaction/error.jsonl",
"rotation": "daily", "rotation": "daily",
"format": "json", "format": "json",
"ansi": false, "ansi": false,
"targets": [ "targets": [
"kb_execution_solana" "kb-lib.executor.solana.transaction"
] ]
}, },
{ {
@@ -3511,7 +3511,7 @@
"kb-lib.decoder.spl.memo", "kb-lib.decoder.spl.memo",
"kb-lib.decoder.spl.token", "kb-lib.decoder.spl.token",
"kb-lib.decoder.spl.token2022", "kb-lib.decoder.spl.token2022",
"kb_execution_solana", "kb-lib.executor.solana.transaction",
"kb_executor_metadata_metaplex_token_metadata", "kb_executor_metadata_metaplex_token_metadata",
"kb_executor_metadata_spl_name_service", "kb_executor_metadata_spl_name_service",
"kb-lib.executor.solana.core", "kb-lib.executor.solana.core",
@@ -3902,42 +3902,42 @@
] ]
}, },
{ {
"name": "file_kb_execution_solana_debug", "name": "file_kb_lib_executor_solana_transaction_debug",
"enabled": true, "enabled": true,
"sink": "file", "sink": "file",
"level": "debug", "level": "debug",
"path": "logs/mainnet/kb_execution_solana/debug.log", "path": "logs/mainnet/kb-lib/executor/solana/transaction/debug.log",
"rotation": "daily", "rotation": "daily",
"format": "human", "format": "human",
"ansi": false, "ansi": false,
"targets": [ "targets": [
"kb_execution_solana" "kb-lib.executor.solana.transaction"
] ]
}, },
{ {
"name": "file_kb_execution_solana_info", "name": "file_kb_lib_executor_solana_transaction_info",
"enabled": true, "enabled": true,
"sink": "file", "sink": "file",
"level": "info", "level": "info",
"path": "logs/mainnet/kb_execution_solana/info.log", "path": "logs/mainnet/kb-lib/executor/solana/transaction/info.log",
"rotation": "daily", "rotation": "daily",
"format": "human", "format": "human",
"ansi": false, "ansi": false,
"targets": [ "targets": [
"kb_execution_solana" "kb-lib.executor.solana.transaction"
] ]
}, },
{ {
"name": "file_kb_execution_solana_error", "name": "file_kb_lib_executor_solana_transaction_error",
"enabled": true, "enabled": true,
"sink": "file", "sink": "file",
"level": "error", "level": "error",
"path": "logs/mainnet/kb_execution_solana/error.jsonl", "path": "logs/mainnet/kb-lib/executor/solana/transaction/error.jsonl",
"rotation": "daily", "rotation": "daily",
"format": "json", "format": "json",
"ansi": false, "ansi": false,
"targets": [ "targets": [
"kb_execution_solana" "kb-lib.executor.solana.transaction"
] ]
}, },
{ {

View File

@@ -33,7 +33,7 @@ Une opération dangereuse ne doit pas être supprimée de l'exécuteur. Elle doi
- `kb_execution_api` définit les contrats communs des exécuteurs. - `kb_execution_api` définit les contrats communs des exécuteurs.
- `kb_execution_safety` regroupe les validations avant simulation, signature ou envoi. - `kb_execution_safety` regroupe les validations avant simulation, signature ou envoi.
- `kb_execution_solana` assemble les plans en messages/transactions Solana et orchestre la signature sans RPC. - `kb-lib::executor::solana::transaction` assemble les plans en messages/transactions Solana et orchestre la signature sans RPC.
- `kb_executor_solana_core` construit les instructions des programmes natifs. - `kb_executor_solana_core` construit les instructions des programmes natifs.
- `kb_wallet` isole les secrets et fournit des signataires. - `kb_wallet` isole les secrets et fournit des signataires.
- `kb_rpc` fournit simulation, envoi et confirmation. - `kb_rpc` fournit simulation, envoi et confirmation.
@@ -81,7 +81,7 @@ La simulation accepte une transaction base64 non signée lorsque `sigVerify = fa
## Assemblage et signature Solana ## Assemblage et signature Solana
`0.4.2-pre.005` introduit `kb_execution_solana`, frontière commune entre les exécuteurs et les adaptateurs RPC. La crate convertit les `ExApiPlannedInstruction` en instructions SDK, compile le message avec son fee payer et sa source de blockhash, puis vérifie que les signataires réellement compilés correspondent exactement au contrat du plan. `0.4.2-pre.005` introduit `kb-lib::executor::solana::transaction`, frontière commune entre les exécuteurs et les adaptateurs RPC. La crate convertit les `ExApiPlannedInstruction` en instructions SDK, compile le message avec son fee payer et sa source de blockhash, puis vérifie que les signataires réellement compilés correspondent exactement au contrat du plan.
La transaction non signée fournit deux sorties distinctes : La transaction non signée fournit deux sorties distinctes :
@@ -110,7 +110,7 @@ getAccountInfo complet et borné à la taille nonce officielle
-> signature du même hash de message et de la même valeur nonce -> signature du même hash de message et de la même valeur nonce
``` ```
`kb_executor_solana_core` ajoute lautorité nonce aux signataires requis du plan métier. `kb_execution_solana` refuse les états legacy ou non initialisés, conserve le plan source immuable, expose un plan effectif avec lavance injectée et exige que le SDK reconnaisse la transaction comme durable nonce. La lecture RPC, lassemblage, la simulation et la signature restent des étapes séparées. `kb_executor_solana_core` ajoute lautorité nonce aux signataires requis du plan métier. `kb-lib::executor::solana::transaction` refuse les états legacy ou non initialisés, conserve le plan source immuable, expose un plan effectif avec lavance injectée et exige que le SDK reconnaisse la transaction comme durable nonce. La lecture RPC, lassemblage, la simulation et la signature restent des étapes séparées.
### Builders Address Lookup Table — `pre.015` ### Builders Address Lookup Table — `pre.015`

View File

@@ -21,13 +21,13 @@ Cette matrice suit la symétrie entre surfaces de décodage et surfaces de const
## Socle dexécution ## Socle dexécution
| Crate | Rôle | | Crate | Rôle |
|-----------------------|------------------------------------------------------------------------------| |-----------------------------------------|------------------------------------------------------------------------------|
| `kb_execution_api` | Contrats provider-neutral des capacités, politiques, plans et résultats. | | `kb_execution_api` | Contrats provider-neutral des capacités, politiques, plans et résultats. |
| `kb_execution_safety` | Garde-fous stateless avant simulation, signature ou envoi. | | `kb_execution_safety` | Garde-fous stateless avant simulation, signature ou envoi. |
| `kb_execution_solana` | Compilation Solana, blockhash/nonce, preuve de simulation et signature. | | `kb-lib::executor::solana::transaction` | Compilation Solana, blockhash/nonce, preuve de simulation et signature. |
| `kb_rpc` | Acquisition, simulation, envoi et confirmation JSON-RPC ; aucune clé privée. | | `kb_rpc` | Acquisition, simulation, envoi et confirmation JSON-RPC ; aucune clé privée. |
| `kb_wallet` | Résolution des signataires derrière un backend explicite. | | `kb_wallet` | Résolution des signataires derrière un backend explicite. |
## Règle mécanique ## Règle mécanique

View File

@@ -18,7 +18,7 @@ Linventaire des anciennes crates métier de `khadhroony-bot2` a été compar
Il reste une ancienne crate fonctionnelle : Il reste une ancienne crate fonctionnelle :
- `kb_execution_solana` : assemblage de transactions legacy, durable nonce, validation des comptes nonce, signature, preuves de simulation et contrôle de taille de paquet. - `kb-lib::executor::solana::transaction` : assemblage de transactions legacy, durable nonce, validation des comptes nonce, signature, preuves de simulation et contrôle de taille de paquet.
Cette couche nest pas un exécuteur de programme. Elle doit être portée sous la famille `executor/solana` avant la migration du transport on-chain. Cette couche nest pas un exécuteur de programme. Elle doit être portée sous la famille `executor/solana` avant la migration du transport on-chain.
@@ -28,4 +28,4 @@ Les autres modules de décodeurs, matérialisateurs et exécuteurs présents dan
### Étape suivante ### Étape suivante
Migrer `kb_execution_solana`, vérifier ses contrats publics et ses tests, puis clôturer la parité de `kb-lib` avant de passer à la future crate de transport on-chain. La couche `kb-lib::executor::solana::transaction` est migrée. Après validation utilisateur de cette tranche, la parité fonctionnelle de `kb-lib` avec bot2 est close avant la future crate de transport on-chain.

View File

@@ -31,12 +31,12 @@ de migration restent internes à leur inventaire et ne font pas partie de lAP
## Nomenclature de `kb-lib` ## Nomenclature de `kb-lib`
| Famille | Constante | Type ou trait | Fonction libre | | Famille | Constante | Type ou trait | Fonction libre |
|---|---|---|---| |---------------|-----------|---------------|-----------------|
| Décodeur | `DC_` | `Dc` | `decoder_` | | Décodeur | `DC_` | `Dc` | `decoder_` |
| Matérialiseur | `MT_` | `Mt` | `materializer_` | | Matérialiseur | `MT_` | `Mt` | `materializer_` |
| Exécuteur | `EX_` | `Ex` | `executor_` | | Exécuteur | `EX_` | `Ex` | `executor_` |
| Modèle | `MD_` | `Md` | `model_` | | Modèle | `MD_` | `Md` | `model_` |
Les méthodes inhérentes et les helpers strictement privés ne sont pas soumis à un préfixe Les méthodes inhérentes et les helpers strictement privés ne sont pas soumis à un préfixe
global. Les symboles `pub` et `pub(crate)` le sont, car ils peuvent entrer en collision après global. Les symboles `pub` et `pub(crate)` le sont, car ils peuvent entrer en collision après

View File

@@ -63,9 +63,9 @@ Les crates officielles Ed25519, secp256k1 et secp256r1 exposent les IDs, constan
`kb_decoder_solana_core` reproduit les layouts exacts depuis les sources runtime Agave : table Ed25519/secp256r1 de 14 octets, table secp256k1 de 11 octets, sentinelle `u16::MAX` uniquement pour Ed25519/secp256r1, index `u8` toujours explicite pour secp256k1, règles zéro signature et limite secp256r1. Les fixtures manuelles rendent chaque offset visible et les tests couvrent les références inter-instructions et les bornes. Aucune dépendance `bincode` nest ajoutée. `kb_decoder_solana_core` reproduit les layouts exacts depuis les sources runtime Agave : table Ed25519/secp256r1 de 14 octets, table secp256k1 de 11 octets, sentinelle `u16::MAX` uniquement pour Ed25519/secp256r1, index `u8` toujours explicite pour secp256k1, règles zéro signature et limite secp256r1. Les fixtures manuelles rendent chaque offset visible et les tests couvrent les références inter-instructions et les bornes. Aucune dépendance `bincode` nest ajoutée.
## Transaction client dans `kb_execution_solana` ## Transaction client dans `kb-lib::executor::solana::transaction`
`kb_execution_solana` nimporte plus lagrégat `solana-sdk`. Sa frontière transactionnelle utilise les crates modulaires réellement consommées : `solana-instruction`, `solana-message`, `solana-transaction`, `solana-hash`, `solana-pubkey`, `solana-keypair`, `solana-signer`, `solana-nonce` et `solana-system-interface`. Les exécuteurs spécifiques conservent la même politique et ajoutent uniquement linterface de programme nécessaire. `kb-lib::executor::solana::transaction` nimporte plus lagrégat `solana-sdk`. Sa frontière transactionnelle utilise les crates modulaires réellement consommées : `solana-instruction`, `solana-message`, `solana-transaction`, `solana-hash`, `solana-pubkey`, `solana-keypair`, `solana-signer`, `solana-nonce` et `solana-system-interface`. Les exécuteurs spécifiques conservent la même politique et ajoutent uniquement linterface de programme nécessaire.
Le message à signer provient de `solana_transaction::Transaction::message_data()`. La transaction complète utilise `wincode::serialize`, supporté officiellement par les types modulaires `Message` et `Transaction`. Cette stratégie remplace tout appel direct à `bincode` et reproduit le wire attendu par `getFeeForMessage`, `simulateTransaction` et `sendTransaction`. La taille sérialisée est refusée au-delà de 1 232 octets. Le message à signer provient de `solana_transaction::Transaction::message_data()`. La transaction complète utilise `wincode::serialize`, supporté officiellement par les types modulaires `Message` et `Transaction`. Cette stratégie remplace tout appel direct à `bincode` et reproduit le wire attendu par `getFeeForMessage`, `simulateTransaction` et `sendTransaction`. La taille sérialisée est refusée au-delà de 1 232 octets.
@@ -95,7 +95,7 @@ Le message à signer provient de `solana_transaction::Transaction::message_data(
`kb_decoder_spl_memo` et `kb_executor_spl_memo` consomment `spl-memo-interface ^2.1` sans feature additionnelle. La version effectivement résolue pendant les validations est 2.1.0. Elle publie les trois IDs exacts et un builder générique recevant explicitement le Program ID. Ce builder place les octets exacts du message dans `Instruction::data` et transforme chaque pubkey fournie en compte readonly signer, sans sérialisation intermédiaire. `kb_decoder_spl_memo` et `kb_executor_spl_memo` consomment `spl-memo-interface ^2.1` sans feature additionnelle. La version effectivement résolue pendant les validations est 2.1.0. Elle publie les trois IDs exacts et un builder générique recevant explicitement le Program ID. Ce builder place les octets exacts du message dans `Instruction::data` et transforme chaque pubkey fournie en compte readonly signer, sans sérialisation intermédiaire.
Lexécuteur appelle ce builder pour les trois générations et compare chaque plan au résultat officiel. Les différences runtime restent explicites : v1 ignore les comptes, v3/v4 les vérifient. La bibliothèque ne lie toutefois aucune génération à un cluster ou au dry-run : la simulation obligatoire établit la disponibilité effective du Program ID ciblé, puis les politiques communes gouvernent lautorisation denvoi et Mainnet. La démo du jalon retient séparément v4 sur Devnet. La taille packet finale reste contrôlée par `kb_execution_solana` après compilation du message. Lexécuteur appelle ce builder pour les trois générations et compare chaque plan au résultat officiel. Les différences runtime restent explicites : v1 ignore les comptes, v3/v4 les vérifient. La bibliothèque ne lie toutefois aucune génération à un cluster ou au dry-run : la simulation obligatoire établit la disponibilité effective du Program ID ciblé, puis les politiques communes gouvernent lautorisation denvoi et Mainnet. La démo du jalon retient séparément v4 sur Devnet. La taille packet finale reste contrôlée par `kb-lib::executor::solana::transaction` après compilation du message.
Le décodeur ne dépend pas du builder pour lire le wire : il analyse directement les octets base64 retenus par le core, avec une borne de 4 096 octets, puis valide lUTF8. La source historique v1 prouve que les comptes sont ignorés ; les processors v3/v4 exigent au contraire que chaque compte fourni soit signer avant de valider lUTF8. Ces règles restent séparées dans `docs/SPL_MEMO_MATRIX.json` et les tests comparent les trois IDs locaux aux constantes de linterface officielle. Le décodeur ne dépend pas du builder pour lire le wire : il analyse directement les octets base64 retenus par le core, avec une borne de 4 096 octets, puis valide lUTF8. La source historique v1 prouve que les comptes sont ignorés ; les processors v3/v4 exigent au contraire que chaque compte fourni soit signer avant de valider lUTF8. Ces règles restent séparées dans `docs/SPL_MEMO_MATRIX.json` et les tests comparent les trois IDs locaux aux constantes de linterface officielle.

View File

@@ -77,20 +77,20 @@ Le navigateur SQL applique maintenant la limite maximale reçue du backend avant
Validations fournies le 13 juillet 2026 : Validations fournies le 13 juillet 2026 :
| Crate / contrôle | Résultat | | Crate / contrôle | Résultat |
|------------------------------------|----------:| |-----------------------------------------|----------:|
| `kb_program_ids` | 5 tests | | `kb_program_ids` | 5 tests |
| `kb_decoder_solana_core` | 116 tests | | `kb_decoder_solana_core` | 116 tests |
| `kb_executor_solana_core` | 88 tests | | `kb_executor_solana_core` | 88 tests |
| `kb_execution_api` | 22 tests | | `kb_execution_api` | 22 tests |
| `kb_execution_safety` | 15 tests | | `kb_execution_safety` | 15 tests |
| `kb_execution_solana` | 12 tests | | `kb-lib::executor::solana::transaction` | 12 tests |
| `kb_rpc` | 113 tests | | `kb_rpc` | 113 tests |
| `kb_pipeline` | 56 tests | | `kb_pipeline` | 56 tests |
| `kb_config` | 41 tests | | `kb_config` | 41 tests |
| `kb_app_demo` | 88 tests | | `kb_app_demo` | 88 tests |
| `kb_store_pg` avec PostgreSQL réel | 45 tests | | `kb_store_pg` avec PostgreSQL réel | 45 tests |
| `cargo clippy --all-targets` | propre | | `cargo clippy --all-targets` | propre |
Le test Devnet opt-in a exécuté un transfert de 1 000 000 lamports avec airdrop nul depuis un wallet préfinancé. Il a confirmé simulation, signature, envoi, confirmation, hydratation canonique, extraction core et decode replay. `cargo tauri dev` a démarré Vite, initialisé les treize tables attendues et exercé les sessions WebSocket. Le test Devnet opt-in a exécuté un transfert de 1 000 000 lamports avec airdrop nul depuis un wallet préfinancé. Il a confirmé simulation, signature, envoi, confirmation, hydratation canonique, extraction core et decode replay. `cargo tauri dev` a démarré Vite, initialisé les treize tables attendues et exercé les sessions WebSocket.

View File

@@ -1,5 +1,5 @@
# file: kb-lib/Cargo.toml # file: kb-lib/Cargo.toml
# version: 13 # version: 14
[package] [package]
name = "kb-lib" name = "kb-lib"
@@ -25,6 +25,11 @@ solana-compute-budget-interface.workspace = true
solana-feature-gate-interface.workspace = true solana-feature-gate-interface.workspace = true
solana-hash.workspace = true solana-hash.workspace = true
solana-instruction.workspace = true solana-instruction.workspace = true
solana-transaction.workspace = true
solana-signer.workspace = true
solana-nonce.workspace = true
solana-message.workspace = true
solana-keypair.workspace = true
solana-loader-v3-interface.workspace = true solana-loader-v3-interface.workspace = true
solana-pubkey.workspace = true solana-pubkey.workspace = true
solana-sdk-ids.workspace = true solana-sdk-ids.workspace = true

View File

@@ -1,5 +1,5 @@
// file: kb-lib/src/executor.rs // file: kb-lib/src/executor.rs
// version: 7 // version: 8
//! Consolidated executor modules. //! Consolidated executor modules.
@@ -493,6 +493,22 @@ pub use self::solana::ExSolanaCoreVoteLockout;
pub use self::solana::ExSolanaCoreZkElGamalContextState; pub use self::solana::ExSolanaCoreZkElGamalContextState;
/// Exposes the official ZK ElGamal proof kinds. /// Exposes the official ZK ElGamal proof kinds.
pub use self::solana::ExSolanaCoreZkElGamalProofType; pub use self::solana::ExSolanaCoreZkElGamalProofType;
/// Validated current durable nonce account state used for transaction assembly.
pub use self::solana::ExSolanaDurableNonceAccountState;
/// Signed Solana transaction ready for RPC submission.
pub use self::solana::ExSolanaSignedTransaction;
/// Simulation result bound to the exact assembled message that was simulated.
pub use self::solana::ExSolanaSimulationEvidence;
/// Unsigned assembled Solana transaction used for fee estimation and simulation.
pub use self::solana::ExSolanaUnsignedTransaction;
/// Builds an unsigned durable nonce transaction from a prepared execution plan and validated nonce state.
pub use self::solana::executor_solana_build_durable_nonce_transaction;
/// Builds an unsigned legacy Solana transaction from a prepared execution plan.
pub use self::solana::executor_solana_build_legacy_transaction;
/// Returns the exact serialized byte length of a Solana nonce account state.
pub use self::solana::executor_solana_durable_nonce_account_data_length;
/// Parses and validates a complete System Program-owned durable nonce account snapshot.
pub use self::solana::executor_solana_parse_durable_nonce_account;
/// Exposes the idempotent ATA creation operation code. /// Exposes the idempotent ATA creation operation code.
pub use self::spl::EX_SPL_ATA_CREATE_IDEMPOTENT_OPERATION; pub use self::spl::EX_SPL_ATA_CREATE_IDEMPOTENT_OPERATION;
/// Exposes the strict ATA creation operation code. /// Exposes the strict ATA creation operation code.
@@ -884,8 +900,14 @@ pub use self::wallet::ExWalletJupiterApeproSmartWalletExecutor;
/// Exposes the reserved `weighted/swap_stabble` executor. /// Exposes the reserved `weighted/swap_stabble` executor.
pub use self::weighted::ExWeightedSwapStabbleExecutor; pub use self::weighted::ExWeightedSwapStabbleExecutor;
/// Stable operation code for the nonce advance injected by durable transaction assembly.
pub(crate) use self::solana::EX_SOLANA_TRANSACTION_DURABLE_NONCE_ADVANCE_OPERATION;
/// Maximum over-the-wire size of a Solana transaction packet.
pub(crate) use self::solana::EX_SOLANA_TRANSACTION_MAX_WIRE_BYTES;
/// Canonical tracing target for this crate. /// Canonical tracing target for this crate.
pub(crate) use self::solana::TRACING_TARGET_EXECUTOR_SOLANA_CORE; pub(crate) use self::solana::TRACING_TARGET_EXECUTOR_SOLANA_CORE;
/// Canonical tracing target for Solana transaction assembly.
pub(crate) use self::solana::TRACING_TARGET_EXECUTOR_SOLANA_TRANSACTION;
/// Crate-root access to `build_prepared_plan` from `address_lookup_table`. /// Crate-root access to `build_prepared_plan` from `address_lookup_table`.
pub(crate) use self::solana::executor_solana_core_address_lookup_table_build_prepared_plan; pub(crate) use self::solana::executor_solana_core_address_lookup_table_build_prepared_plan;
/// Crate-root access to `build_prepared_plan` from `builder`. /// Crate-root access to `build_prepared_plan` from `builder`.

View File

@@ -1,9 +1,10 @@
// file: kb-lib/src/executor/solana.rs // file: kb-lib/src/executor/solana.rs
// version: 4 // version: 5
//! `solana` executor family. //! `solana` executor family.
mod core; mod core;
mod transaction;
/// Exposes the Address Lookup Table close operation code. /// Exposes the Address Lookup Table close operation code.
pub use self::core::EX_SOLANA_CORE_ADDRESS_LOOKUP_TABLE_CLOSE_OPERATION; pub use self::core::EX_SOLANA_CORE_ADDRESS_LOOKUP_TABLE_CLOSE_OPERATION;
@@ -253,6 +254,22 @@ pub use self::core::ExSolanaCoreVoteLockout;
pub use self::core::ExSolanaCoreZkElGamalContextState; pub use self::core::ExSolanaCoreZkElGamalContextState;
/// Exposes the official ZK ElGamal proof kinds. /// Exposes the official ZK ElGamal proof kinds.
pub use self::core::ExSolanaCoreZkElGamalProofType; pub use self::core::ExSolanaCoreZkElGamalProofType;
/// Validated current durable nonce account state used for transaction assembly.
pub use self::transaction::ExSolanaDurableNonceAccountState;
/// Signed Solana transaction ready for RPC submission.
pub use self::transaction::ExSolanaSignedTransaction;
/// Simulation result bound to the exact assembled message that was simulated.
pub use self::transaction::ExSolanaSimulationEvidence;
/// Unsigned assembled Solana transaction used for fee estimation and simulation.
pub use self::transaction::ExSolanaUnsignedTransaction;
/// Builds an unsigned durable nonce transaction from a prepared execution plan and validated nonce state.
pub use self::transaction::executor_solana_build_durable_nonce_transaction;
/// Builds an unsigned legacy Solana transaction from a prepared execution plan.
pub use self::transaction::executor_solana_build_legacy_transaction;
/// Returns the exact serialized byte length of a Solana nonce account state.
pub use self::transaction::executor_solana_durable_nonce_account_data_length;
/// Parses and validates a complete System Program-owned durable nonce account snapshot.
pub use self::transaction::executor_solana_parse_durable_nonce_account;
/// Canonical tracing target for this crate. /// Canonical tracing target for this crate.
pub(crate) use self::core::TRACING_TARGET_EXECUTOR_SOLANA_CORE; pub(crate) use self::core::TRACING_TARGET_EXECUTOR_SOLANA_CORE;
@@ -292,3 +309,9 @@ pub(crate) use self::core::executor_solana_core_validate_seeded_address;
pub(crate) use self::core::executor_solana_core_vote_build_prepared_plan; pub(crate) use self::core::executor_solana_core_vote_build_prepared_plan;
/// Crate-root access to `build_prepared_plan` from `zk_elgamal`. /// Crate-root access to `build_prepared_plan` from `zk_elgamal`.
pub(crate) use self::core::executor_solana_core_zk_elgamal_build_prepared_plan; pub(crate) use self::core::executor_solana_core_zk_elgamal_build_prepared_plan;
/// Stable operation code for the nonce advance injected by durable transaction assembly.
pub(crate) use self::transaction::EX_SOLANA_TRANSACTION_DURABLE_NONCE_ADVANCE_OPERATION;
/// Maximum over-the-wire size of a Solana transaction packet.
pub(crate) use self::transaction::EX_SOLANA_TRANSACTION_MAX_WIRE_BYTES;
/// Canonical tracing target for Solana transaction assembly.
pub(crate) use self::transaction::TRACING_TARGET_EXECUTOR_SOLANA_TRANSACTION;

View File

@@ -0,0 +1,32 @@
// file: kb-lib/src/executor/solana/transaction.rs
// version: 2
//! Solana transaction assembly and signing for prepared execution plans.
mod constants;
mod core;
mod nonce;
/// Signed Solana transaction ready for RPC submission.
pub use self::core::ExSolanaSignedTransaction;
/// Simulation result bound to the exact assembled message that was simulated.
pub use self::core::ExSolanaSimulationEvidence;
/// Unsigned assembled Solana transaction used for fee estimation and simulation.
pub use self::core::ExSolanaUnsignedTransaction;
/// Builds an unsigned durable nonce transaction from a prepared execution plan and validated nonce state.
pub use self::core::executor_solana_build_durable_nonce_transaction;
/// Builds an unsigned legacy Solana transaction from a prepared execution plan.
pub use self::core::executor_solana_build_legacy_transaction;
/// Validated current durable nonce account state used for transaction assembly.
pub use self::nonce::ExSolanaDurableNonceAccountState;
/// Returns the exact serialized byte length of a Solana nonce account state.
pub use self::nonce::executor_solana_durable_nonce_account_data_length;
/// Parses and validates a complete System Program-owned durable nonce account snapshot.
pub use self::nonce::executor_solana_parse_durable_nonce_account;
/// Stable operation code for the nonce advance injected by durable transaction assembly.
pub(crate) use self::constants::EX_SOLANA_TRANSACTION_DURABLE_NONCE_ADVANCE_OPERATION;
/// Maximum over-the-wire size of a Solana transaction packet.
pub(crate) use self::constants::EX_SOLANA_TRANSACTION_MAX_WIRE_BYTES;
/// Canonical tracing target for Solana transaction assembly.
pub(crate) use self::constants::TRACING_TARGET_EXECUTOR_SOLANA_TRANSACTION;

View File

@@ -0,0 +1,13 @@
// file: kb-lib/src/executor/solana/transaction/constants.rs
// version: 2
//! Local constants for Solana transaction assembly.
/// Stable operation code for the nonce advance injected by durable transaction assembly.
pub(crate) const EX_SOLANA_TRANSACTION_DURABLE_NONCE_ADVANCE_OPERATION: &str =
"solana_core.system.nonce.advance";
/// Maximum over-the-wire size of a Solana transaction packet.
pub(crate) const EX_SOLANA_TRANSACTION_MAX_WIRE_BYTES: usize = 1_232;
/// Canonical tracing target for Solana transaction assembly.
pub(crate) const TRACING_TARGET_EXECUTOR_SOLANA_TRANSACTION: &str =
"kb-lib.executor.solana.transaction";

View File

@@ -0,0 +1,988 @@
// file: kb-lib/src/executor/solana/transaction/core.rs
// version: 9
//! Legacy and durable nonce Solana transaction assembly, simulation binding and signing.
use base64::Engine; // rust-rules: trait-import
use std::str::FromStr; // rust-rules: trait-import
/// Unsigned assembled Solana transaction used for fee estimation and simulation.
#[derive(Clone, Debug)]
pub struct ExSolanaUnsignedTransaction {
plan: crate::ExApiPreparedExecutionPlan,
transaction: solana_transaction::Transaction,
message_hash: std::string::String,
blockhash: std::string::String,
required_signer_pubkeys: std::vec::Vec<std::string::String>,
}
impl crate::ExSolanaUnsignedTransaction {
/// Returns the effective immutable plan represented by this transaction.
///
/// Durable nonce transactions include the injected nonce-advance instruction.
pub fn plan(&self) -> &crate::ExApiPreparedExecutionPlan {
return &self.plan;
}
/// Returns the base58 hash of the exact compiled transaction message.
pub fn message_hash(&self) -> &str {
return self.message_hash.as_str();
}
/// Returns the recent blockhash or durable nonce value compiled into the message.
pub fn blockhash(&self) -> &str {
return self.blockhash.as_str();
}
/// Returns required signer public keys in compiled message order.
pub fn required_signer_pubkeys(&self) -> &[std::string::String] {
return self.required_signer_pubkeys.as_slice();
}
/// Returns the serialized message in base64 for `getFeeForMessage`.
pub fn message_base64(&self) -> std::string::String {
return base64::engine::general_purpose::STANDARD.encode(self.transaction.message_data());
}
/// Returns the unsigned serialized transaction in base64 for `simulateTransaction` with `sigVerify = false`.
pub fn transaction_base64(&self) -> kb_core::Result<std::string::String> {
let wire_bytes = match serialize_transaction(&self.transaction) {
std::result::Result::Ok(wire_bytes) => wire_bytes,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(
base64::engine::general_purpose::STANDARD.encode(wire_bytes),
);
}
/// Binds a typed RPC simulation result to this exact compiled message.
pub fn bind_simulation(
&self,
simulation: crate::ExApiExecutionSimulationResult,
) -> crate::ExSolanaSimulationEvidence {
return crate::ExSolanaSimulationEvidence {
message_hash: self.message_hash.clone(),
blockhash: self.blockhash.clone(),
simulation,
};
}
/// Signs the transaction after safety validation of a matching simulation result.
pub fn sign_after_simulation(
mut self,
evidence: &crate::ExSolanaSimulationEvidence,
signers: &[&dyn solana_signer::Signer],
) -> kb_core::Result<crate::ExSolanaSignedTransaction> {
if evidence.message_hash != self.message_hash {
return std::result::Result::Err(kb_core::Error::new(
"execution_simulation_message_mismatch",
"the simulation result is not bound to the transaction message being signed",
));
}
if evidence.blockhash != self.blockhash {
return std::result::Result::Err(kb_core::Error::new(
"execution_simulation_blockhash_mismatch",
"the simulation result is not bound to the blockhash or durable nonce value being signed",
));
}
let safety_evaluation =
match crate::ExSafetyChecker.evaluate_send(&self.plan, &evidence.simulation) {
std::result::Result::Ok(evaluation) => evaluation,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if safety_evaluation.decision != crate::ExSafetyDecision::Allow {
return std::result::Result::Err(kb_core::Error::new(
"execution_signing_not_authorized",
format!(
"execution safety decision {:?} forbids signing: {}",
safety_evaluation.decision,
violation_codes(&safety_evaluation.violations)
),
));
}
let signers = match resolve_signers(self.required_signer_pubkeys.as_slice(), signers) {
std::result::Result::Ok(signers) => signers,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let recent_blockhash = self.transaction.message.recent_blockhash;
if let std::result::Result::Err(error) =
self.transaction.try_sign(signers.as_slice(), recent_blockhash)
{
return std::result::Result::Err(kb_core::Error::new(
"execution_transaction_sign_failed",
error.to_string(),
));
}
if !self.transaction.is_signed() {
return std::result::Result::Err(kb_core::Error::new(
"execution_transaction_not_fully_signed",
"the transaction is not fully signed after signer resolution",
));
}
let wire_bytes = match serialize_transaction(&self.transaction) {
std::result::Result::Ok(wire_bytes) => wire_bytes,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let primary_signature = match self.transaction.signatures.first() {
std::option::Option::Some(signature) => signature.to_string(),
std::option::Option::None => {
return std::result::Result::Err(kb_core::Error::new(
"execution_primary_signature_missing",
"the signed transaction contains no primary signature",
));
},
};
tracing::info!(
target: crate::TRACING_TARGET_EXECUTOR_SOLANA_TRANSACTION,
action = "sign_transaction",
intent_id = %self.plan.intent_id,
operation_code = %self.plan.operation_code,
message_hash = %self.message_hash,
signer_count = self.required_signer_pubkeys.len(),
wire_length = wire_bytes.len(),
"signed prepared Solana transaction"
);
return std::result::Result::Ok(crate::ExSolanaSignedTransaction {
transaction: self.transaction,
message_hash: self.message_hash,
primary_signature: crate::MdSignature(primary_signature),
signer_pubkeys: self.required_signer_pubkeys,
wire_bytes,
});
}
}
/// Simulation result bound to the exact assembled message that was simulated.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ExSolanaSimulationEvidence {
message_hash: std::string::String,
blockhash: std::string::String,
simulation: crate::ExApiExecutionSimulationResult,
}
impl crate::ExSolanaSimulationEvidence {
/// Returns the base58 hash of the simulated compiled message.
pub fn message_hash(&self) -> &str {
return self.message_hash.as_str();
}
/// Returns the recent blockhash or durable nonce value used by the simulated message.
pub fn blockhash(&self) -> &str {
return self.blockhash.as_str();
}
/// Returns the typed simulation result.
pub fn simulation(&self) -> &crate::ExApiExecutionSimulationResult {
return &self.simulation;
}
}
/// Signed Solana transaction ready for RPC submission.
#[derive(Clone, Debug)]
pub struct ExSolanaSignedTransaction {
transaction: solana_transaction::Transaction,
message_hash: std::string::String,
primary_signature: crate::MdSignature,
signer_pubkeys: std::vec::Vec<std::string::String>,
wire_bytes: std::vec::Vec<u8>,
}
impl crate::ExSolanaSignedTransaction {
/// Returns the base58 hash of the signed compiled message.
pub fn message_hash(&self) -> &str {
return self.message_hash.as_str();
}
/// Returns the transaction's primary signature.
pub fn primary_signature(&self) -> &crate::MdSignature {
return &self.primary_signature;
}
/// Returns signer public keys in signature order.
pub fn signer_pubkeys(&self) -> &[std::string::String] {
return self.signer_pubkeys.as_slice();
}
/// Returns the signed transaction wire length.
pub fn wire_length(&self) -> usize {
return self.wire_bytes.len();
}
/// Returns the signed transaction in base64 for future RPC submission.
pub fn transaction_base64(&self) -> std::string::String {
return base64::engine::general_purpose::STANDARD.encode(self.wire_bytes.as_slice());
}
/// Verifies every signature against the compiled message.
pub fn verify_signatures(&self) -> kb_core::Result<()> {
return match self.transaction.verify() {
std::result::Result::Ok(()) => std::result::Result::Ok(()),
std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::new(
"execution_transaction_signature_verification_failed",
error.to_string(),
)),
};
}
}
/// Builds an unsigned legacy Solana transaction from a prepared execution plan.
pub fn executor_solana_build_legacy_transaction(
plan: &crate::ExApiPreparedExecutionPlan,
recent_blockhash: &str,
) -> kb_core::Result<crate::ExSolanaUnsignedTransaction> {
if plan.policy.blockhash.kind != crate::ExApiExecutionBlockhashKind::Latest {
return std::result::Result::Err(kb_core::Error::new(
"execution_durable_nonce_requires_dedicated_assembly",
"durable nonce plans require executor_solana_build_durable_nonce_transaction",
));
}
if plan.instructions.is_empty() {
return std::result::Result::Err(kb_core::Error::new(
"execution_plan_has_no_instructions",
"cannot assemble a Solana transaction without instructions",
));
}
let fee_payer = match parse_pubkey(plan.fee_payer.0.as_str(), "execution fee payer") {
std::result::Result::Ok(fee_payer) => fee_payer,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let blockhash = match solana_hash::Hash::from_str(recent_blockhash) {
std::result::Result::Ok(blockhash) => blockhash,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::new(
"execution_recent_blockhash_invalid",
format!("invalid recent blockhash: {error}"),
));
},
};
let mut instructions = std::vec::Vec::with_capacity(plan.instructions.len());
for planned_instruction in &plan.instructions {
let instruction = match convert_instruction(planned_instruction) {
std::result::Result::Ok(instruction) => instruction,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
instructions.push(instruction);
}
let message = solana_message::Message::new_with_blockhash(
instructions.as_slice(),
std::option::Option::Some(&fee_payer),
&blockhash,
);
let compiled_signers = message
.signer_keys()
.iter()
.map(|pubkey| return pubkey.to_string())
.collect::<std::vec::Vec<_>>();
match validate_compiled_signers(plan, compiled_signers.as_slice()) {
std::result::Result::Ok(()) => {},
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
let transaction = solana_transaction::Transaction::new_unsigned(message);
let wire_bytes = match serialize_transaction(&transaction) {
std::result::Result::Ok(wire_bytes) => wire_bytes,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let message_hash = transaction.message.hash().to_string();
tracing::debug!(
target: crate::TRACING_TARGET_EXECUTOR_SOLANA_TRANSACTION,
action = "assemble_transaction",
intent_id = %plan.intent_id,
operation_code = %plan.operation_code,
instruction_count = plan.instructions.len(),
signer_count = compiled_signers.len(),
message_hash = %message_hash,
wire_length = wire_bytes.len(),
"assembled unsigned legacy Solana transaction"
);
return std::result::Result::Ok(crate::ExSolanaUnsignedTransaction {
plan: plan.clone(),
transaction,
message_hash,
blockhash: blockhash.to_string(),
required_signer_pubkeys: compiled_signers,
});
}
/// Builds an unsigned transaction that consumes a validated durable nonce state.
pub fn executor_solana_build_durable_nonce_transaction(
plan: &crate::ExApiPreparedExecutionPlan,
nonce_state: &crate::ExSolanaDurableNonceAccountState,
) -> kb_core::Result<crate::ExSolanaUnsignedTransaction> {
if plan.policy.blockhash.kind != crate::ExApiExecutionBlockhashKind::DurableNonce {
return std::result::Result::Err(kb_core::Error::new(
"execution_durable_nonce_policy_required",
"the dedicated durable nonce assembly path requires a DurableNonce blockhash policy",
));
}
if plan.policy.blockhash.max_age_slots.is_some() {
return std::result::Result::Err(kb_core::Error::new(
"execution_durable_nonce_has_blockhash_age",
"durable nonce assembly cannot use a recent blockhash age limit",
));
}
if plan.instructions.is_empty() {
return std::result::Result::Err(kb_core::Error::new(
"execution_plan_has_no_instructions",
"cannot assemble a durable nonce transaction without operation instructions",
));
}
let policy_nonce_account = match &plan.policy.blockhash.nonce_account {
std::option::Option::Some(account) => account,
std::option::Option::None => {
return std::result::Result::Err(kb_core::Error::new(
"execution_durable_nonce_account_missing",
"durable nonce assembly requires the nonce account in the execution policy",
));
},
};
let policy_nonce_authority = match &plan.policy.blockhash.nonce_authority {
std::option::Option::Some(authority) => authority,
std::option::Option::None => {
return std::result::Result::Err(kb_core::Error::new(
"execution_durable_nonce_authority_missing",
"durable nonce assembly requires the nonce authority in the execution policy",
));
},
};
if policy_nonce_account != nonce_state.account() {
return std::result::Result::Err(kb_core::Error::new(
"execution_durable_nonce_account_mismatch",
format!(
"execution policy nonce account {} differs from validated account {}",
policy_nonce_account.0,
nonce_state.account().0
),
));
}
if policy_nonce_authority != nonce_state.authority() {
return std::result::Result::Err(kb_core::Error::new(
"execution_durable_nonce_authority_mismatch",
format!(
"execution policy nonce authority {} differs from validated authority {}",
policy_nonce_authority.0,
nonce_state.authority().0
),
));
}
if !plan
.required_signers
.iter()
.any(|required| return &required.pubkey == policy_nonce_authority)
{
return std::result::Result::Err(kb_core::Error::new(
"execution_durable_nonce_authority_not_declared",
format!(
"durable nonce authority {} is missing from required signers",
policy_nonce_authority.0
),
));
}
let fee_payer = match parse_pubkey(plan.fee_payer.0.as_str(), "execution fee payer") {
std::result::Result::Ok(fee_payer) => fee_payer,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let nonce_account = match parse_pubkey(policy_nonce_account.0.as_str(), "durable nonce account")
{
std::result::Result::Ok(nonce_account) => nonce_account,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let nonce_authority =
match parse_pubkey(policy_nonce_authority.0.as_str(), "durable nonce authority") {
std::result::Result::Ok(nonce_authority) => nonce_authority,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let nonce_blockhash = match solana_hash::Hash::from_str(nonce_state.blockhash()) {
std::result::Result::Ok(blockhash) => blockhash,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::new(
"execution_durable_nonce_blockhash_invalid",
format!("invalid durable nonce value: {error}"),
));
},
};
let advance_instruction = solana_system_interface::instruction::advance_nonce_account(
&nonce_account,
&nonce_authority,
);
let mut effective_plan = plan.clone();
effective_plan.instructions.insert(
0,
planned_instruction(
crate::EX_SOLANA_TRANSACTION_DURABLE_NONCE_ADVANCE_OPERATION,
&advance_instruction,
),
);
let mut operation_instructions = std::vec::Vec::with_capacity(plan.instructions.len());
for planned_instruction in &plan.instructions {
let instruction = match convert_instruction(planned_instruction) {
std::result::Result::Ok(instruction) => instruction,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
operation_instructions.push(instruction);
}
let mut message = solana_message::Message::new_with_nonce(
operation_instructions,
std::option::Option::Some(&fee_payer),
&nonce_account,
&nonce_authority,
);
message.recent_blockhash = nonce_blockhash;
let compiled_signers = message
.signer_keys()
.iter()
.map(|pubkey| return pubkey.to_string())
.collect::<std::vec::Vec<_>>();
match validate_compiled_signers(&effective_plan, compiled_signers.as_slice()) {
std::result::Result::Ok(()) => {},
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
let transaction = solana_transaction::Transaction::new_unsigned(message);
if solana_transaction::uses_durable_nonce(&transaction).is_none() {
return std::result::Result::Err(kb_core::Error::new(
"execution_durable_nonce_marker_missing",
"the assembled transaction is not recognized as a durable nonce transaction",
));
}
let wire_bytes = match serialize_transaction(&transaction) {
std::result::Result::Ok(wire_bytes) => wire_bytes,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let message_hash = transaction.message.hash().to_string();
tracing::debug!(
target: crate::TRACING_TARGET_EXECUTOR_SOLANA_TRANSACTION,
action = "assemble_durable_nonce_transaction",
intent_id = %effective_plan.intent_id,
operation_code = %effective_plan.operation_code,
nonce_account = %policy_nonce_account.0,
nonce_authority = %policy_nonce_authority.0,
nonce_blockhash = %nonce_state.blockhash(),
instruction_count = effective_plan.instructions.len(),
signer_count = compiled_signers.len(),
message_hash = %message_hash,
wire_length = wire_bytes.len(),
"assembled unsigned durable nonce Solana transaction"
);
return std::result::Result::Ok(crate::ExSolanaUnsignedTransaction {
plan: effective_plan,
transaction,
message_hash,
blockhash: nonce_state.blockhash().to_string(),
required_signer_pubkeys: compiled_signers,
});
}
fn convert_instruction(
planned: &crate::ExApiPlannedInstruction,
) -> kb_core::Result<solana_instruction::Instruction> {
let program_id =
match parse_pubkey(planned.program_id.0.as_str(), "planned instruction program id") {
std::result::Result::Ok(program_id) => program_id,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut accounts = std::vec::Vec::with_capacity(planned.accounts.len());
for planned_account in &planned.accounts {
let pubkey =
match parse_pubkey(planned_account.pubkey.0.as_str(), "planned instruction account") {
std::result::Result::Ok(pubkey) => pubkey,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
accounts.push(solana_instruction::AccountMeta {
pubkey,
is_signer: planned_account.is_signer,
is_writable: planned_account.is_writable,
});
}
return std::result::Result::Ok(solana_instruction::Instruction {
program_id,
accounts,
data: planned.data.clone(),
});
}
fn planned_instruction(
operation_code: &str,
instruction: &solana_instruction::Instruction,
) -> crate::ExApiPlannedInstruction {
return crate::ExApiPlannedInstruction {
program_id: crate::MdProgramId(instruction.program_id.to_string()),
operation_code: operation_code.to_string(),
accounts: instruction
.accounts
.iter()
.map(|account| {
return crate::ExApiPlannedAccount {
pubkey: crate::MdPubkey(account.pubkey.to_string()),
is_signer: account.is_signer,
is_writable: account.is_writable,
};
})
.collect(),
data: instruction.data.clone(),
};
}
fn parse_pubkey(value: &str, field_name: &str) -> kb_core::Result<solana_pubkey::Pubkey> {
return match solana_pubkey::Pubkey::from_str(value) {
std::result::Result::Ok(pubkey) => std::result::Result::Ok(pubkey),
std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::new(
"execution_pubkey_invalid",
format!("invalid {field_name} {value}: {error}"),
)),
};
}
fn validate_compiled_signers(
plan: &crate::ExApiPreparedExecutionPlan,
compiled_signers: &[std::string::String],
) -> kb_core::Result<()> {
let declared_signers = plan
.required_signers
.iter()
.map(|required| return required.pubkey.0.as_str())
.collect::<std::collections::BTreeSet<_>>();
let compiled_signer_set = compiled_signers
.iter()
.map(std::string::String::as_str)
.collect::<std::collections::BTreeSet<_>>();
if declared_signers != compiled_signer_set {
return std::result::Result::Err(kb_core::Error::new(
"execution_compiled_signer_contract_mismatch",
format!(
"declared signers [{}] differ from compiled signers [{}]",
declared_signers.into_iter().collect::<std::vec::Vec<_>>().join(","),
compiled_signer_set.into_iter().collect::<std::vec::Vec<_>>().join(",")
),
));
}
return std::result::Result::Ok(());
}
fn resolve_signers<'a>(
required_signers: &[std::string::String],
signers: &'a [&'a dyn solana_signer::Signer],
) -> kb_core::Result<std::vec::Vec<&'a dyn solana_signer::Signer>> {
let mut signer_by_pubkey = std::collections::BTreeMap::new();
for signer in signers {
let public_key = signer.pubkey().to_string();
if signer_by_pubkey.insert(public_key.clone(), *signer).is_some() {
return std::result::Result::Err(kb_core::Error::new(
"execution_signer_duplicate",
format!("signer {public_key} was supplied more than once"),
));
}
}
if signer_by_pubkey.len() != required_signers.len() {
return std::result::Result::Err(kb_core::Error::new(
"execution_signer_count_mismatch",
format!(
"{} signers were supplied for {} required signers",
signer_by_pubkey.len(),
required_signers.len()
),
));
}
let mut resolved = std::vec::Vec::with_capacity(required_signers.len());
for required_signer in required_signers {
let signer = match signer_by_pubkey.get(required_signer) {
std::option::Option::Some(signer) => *signer,
std::option::Option::None => {
return std::result::Result::Err(kb_core::Error::new(
"execution_required_signer_missing",
format!("required signer {required_signer} was not supplied"),
));
},
};
resolved.push(signer);
}
return std::result::Result::Ok(resolved);
}
fn serialize_transaction(
transaction: &solana_transaction::Transaction,
) -> kb_core::Result<std::vec::Vec<u8>> {
let wire_bytes = match wincode::serialize(transaction) {
std::result::Result::Ok(wire_bytes) => wire_bytes,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::new(
"execution_transaction_serialize_failed",
error.to_string(),
));
},
};
if wire_bytes.len() > crate::EX_SOLANA_TRANSACTION_MAX_WIRE_BYTES {
return std::result::Result::Err(kb_core::Error::new(
"execution_transaction_wire_size_exceeded",
format!(
"serialized transaction length {} exceeds the Solana packet limit {}",
wire_bytes.len(),
crate::EX_SOLANA_TRANSACTION_MAX_WIRE_BYTES
),
));
}
return std::result::Result::Ok(wire_bytes);
}
fn violation_codes(violations: &[crate::ExSafetyViolation]) -> std::string::String {
return violations
.iter()
.map(|violation| return violation.code.as_str())
.collect::<std::vec::Vec<_>>()
.join(",");
}
#[cfg(test)]
mod tests {
use solana_signer::Signer; // rust-rules: trait-import
use std::str::FromStr; // rust-rules: trait-import
fn wallet(_alias: &str) -> solana_keypair::Keypair {
return solana_keypair::Keypair::new();
}
fn transfer_plan(
wallet: &solana_keypair::Keypair,
dry_run: bool,
lamports: u64,
) -> crate::ExApiPreparedExecutionPlan {
let payer = crate::MdPubkey(wallet.pubkey().to_string());
let destination = crate::MdPubkey(solana_keypair::Keypair::new().pubkey().to_string());
let intent = crate::ExSolanaCoreExecutionIntent {
intent_id: std::string::String::from("transfer-test"),
fee_payer: payer.clone(),
operation: crate::ExSolanaCoreOperation::SystemTransfer {
from: payer.clone(),
to: destination,
lamports,
},
policy: crate::ExApiExecutionPolicy {
dry_run,
authorized_signers: vec![payer],
cost_limit: crate::ExApiExecutionCostLimit {
max_spend_lamports: std::option::Option::Some(lamports),
max_fee_lamports: std::option::Option::Some(10_000),
max_compute_unit_price_micro_lamports: std::option::Option::Some(100),
},
..crate::ExApiExecutionPolicy::default()
},
};
return crate::ExApiTypedInstructionExecutor::build_prepared_plan(
&crate::ExSolanaCoreExecutor,
&intent,
)
.unwrap_or_else(|error| panic!("unexpected plan error: {error}"));
}
fn durable_transfer_plan(
payer: &solana_keypair::Keypair,
nonce_authority: &solana_keypair::Keypair,
nonce_account: &crate::MdPubkey,
) -> crate::ExApiPreparedExecutionPlan {
let payer_pubkey = crate::MdPubkey(payer.pubkey().to_string());
let authority_pubkey = crate::MdPubkey(nonce_authority.pubkey().to_string());
let destination = crate::MdPubkey(solana_keypair::Keypair::new().pubkey().to_string());
let intent = crate::ExSolanaCoreExecutionIntent {
intent_id: std::string::String::from("durable-transfer-test"),
fee_payer: payer_pubkey.clone(),
operation: crate::ExSolanaCoreOperation::SystemTransfer {
from: payer_pubkey.clone(),
to: destination,
lamports: 100,
},
policy: crate::ExApiExecutionPolicy {
dry_run: false,
authorized_signers: vec![payer_pubkey, authority_pubkey.clone()],
blockhash: crate::ExApiExecutionBlockhashPolicy {
kind: crate::ExApiExecutionBlockhashKind::DurableNonce,
max_age_slots: std::option::Option::None,
nonce_account: std::option::Option::Some(nonce_account.clone()),
nonce_authority: std::option::Option::Some(authority_pubkey),
},
cost_limit: crate::ExApiExecutionCostLimit {
max_spend_lamports: std::option::Option::Some(100),
max_fee_lamports: std::option::Option::Some(10_000),
max_compute_unit_price_micro_lamports: std::option::Option::Some(100),
},
..crate::ExApiExecutionPolicy::default()
},
};
return crate::ExApiTypedInstructionExecutor::build_prepared_plan(
&crate::ExSolanaCoreExecutor,
&intent,
)
.unwrap_or_else(|error| panic!("unexpected durable plan error: {error}"));
}
fn nonce_state(
nonce_account: &crate::MdPubkey,
nonce_authority: &solana_keypair::Keypair,
) -> crate::ExSolanaDurableNonceAccountState {
let source_blockhash = solana_hash::Hash::new_unique();
let durable_nonce = solana_nonce::state::DurableNonce::from_blockhash(&source_blockhash);
let authority =
solana_pubkey::Pubkey::from_str(nonce_authority.pubkey().to_string().as_str())
.unwrap_or_else(|error| panic!("invalid authority fixture: {error}"));
let versions = solana_nonce::versions::Versions::new(
solana_nonce::state::State::new_initialized(&authority, durable_nonce, 5_000),
);
let bytes = wincode::serialize(&versions)
.unwrap_or_else(|error| panic!("unexpected nonce serialization error: {error}"));
return crate::executor_solana_parse_durable_nonce_account(
nonce_account,
&crate::MdProgramId(solana_sdk_ids::system_program::id().to_string()),
false,
bytes.len() as u64,
bytes.as_slice(),
)
.unwrap_or_else(|error| panic!("unexpected nonce parse error: {error}"));
}
fn successful_simulation() -> crate::ExApiExecutionSimulationResult {
return crate::ExApiExecutionSimulationResult {
simulated: true,
success: true,
cluster: crate::ExApiExecutionCluster::Devnet,
blockhash_kind: crate::ExApiExecutionBlockhashKind::Latest,
blockhash_age_slots: std::option::Option::Some(1),
replacement_blockhash: std::option::Option::None,
replacement_last_valid_block_height: std::option::Option::None,
nonce_account: std::option::Option::None,
nonce_authority: std::option::Option::None,
units_consumed: std::option::Option::Some(500),
estimated_fee_lamports: std::option::Option::Some(5_000),
logs: std::vec::Vec::new(),
error: std::option::Option::None,
};
}
fn successful_nonce_simulation(
nonce_account: &crate::MdPubkey,
nonce_authority: &solana_keypair::Keypair,
) -> crate::ExApiExecutionSimulationResult {
return crate::ExApiExecutionSimulationResult {
simulated: true,
success: true,
cluster: crate::ExApiExecutionCluster::Devnet,
blockhash_kind: crate::ExApiExecutionBlockhashKind::DurableNonce,
blockhash_age_slots: std::option::Option::None,
replacement_blockhash: std::option::Option::None,
replacement_last_valid_block_height: std::option::Option::None,
nonce_account: std::option::Option::Some(nonce_account.clone()),
nonce_authority: std::option::Option::Some(crate::MdPubkey(
nonce_authority.pubkey().to_string(),
)),
units_consumed: std::option::Option::Some(700),
estimated_fee_lamports: std::option::Option::Some(10_000),
logs: std::vec::Vec::new(),
error: std::option::Option::None,
};
}
#[test]
fn legacy_transaction_preserves_plan_and_serializes_for_rpc() {
let wallet = wallet("payer");
let plan = transfer_plan(&wallet, false, 100);
let blockhash = solana_hash::Hash::new_unique().to_string();
let unsigned = crate::executor_solana_build_legacy_transaction(&plan, blockhash.as_str())
.unwrap_or_else(|error| panic!("unexpected assembly error: {error}"));
assert_eq!(unsigned.plan().intent_id, plan.intent_id);
assert_eq!(unsigned.required_signer_pubkeys(), &[wallet.pubkey().to_string()]);
assert!(!unsigned.message_hash().is_empty());
assert!(!unsigned.message_base64().is_empty());
assert!(
!unsigned
.transaction_base64()
.unwrap_or_else(|error| panic!("unexpected serialization error: {error}"))
.is_empty()
);
}
#[test]
fn matching_simulation_and_signer_produce_verified_signature() {
let wallet = wallet("payer");
let plan = transfer_plan(&wallet, false, 100);
let blockhash = solana_hash::Hash::new_unique().to_string();
let unsigned = crate::executor_solana_build_legacy_transaction(&plan, blockhash.as_str())
.unwrap_or_else(|error| panic!("unexpected assembly error: {error}"));
let evidence = unsigned.bind_simulation(successful_simulation());
let signed = unsigned
.sign_after_simulation(&evidence, &[&wallet])
.unwrap_or_else(|error| panic!("unexpected signing error: {error}"));
signed
.verify_signatures()
.unwrap_or_else(|error| panic!("unexpected verification error: {error}"));
assert!(!signed.primary_signature().0.is_empty());
assert_eq!(signed.signer_pubkeys(), &[wallet.pubkey().to_string()]);
assert!(signed.wire_length() <= crate::EX_SOLANA_TRANSACTION_MAX_WIRE_BYTES);
assert!(!signed.transaction_base64().is_empty());
}
#[test]
fn dry_run_policy_blocks_signing_after_successful_simulation() {
let wallet = wallet("payer");
let plan = transfer_plan(&wallet, true, 100);
let blockhash = solana_hash::Hash::new_unique().to_string();
let unsigned = crate::executor_solana_build_legacy_transaction(&plan, blockhash.as_str())
.unwrap_or_else(|error| panic!("unexpected assembly error: {error}"));
let evidence = unsigned.bind_simulation(successful_simulation());
let result = unsigned.sign_after_simulation(&evidence, &[&wallet]);
assert!(result.is_err());
}
#[test]
fn simulation_evidence_from_another_message_is_rejected() {
let wallet = wallet("payer");
let plan = transfer_plan(&wallet, false, 100);
let first_hash = solana_hash::Hash::new_unique().to_string();
let second_hash = solana_hash::Hash::new_unique().to_string();
let first = crate::executor_solana_build_legacy_transaction(&plan, first_hash.as_str())
.unwrap_or_else(|error| panic!("unexpected first assembly error: {error}"));
let second = crate::executor_solana_build_legacy_transaction(&plan, second_hash.as_str())
.unwrap_or_else(|error| panic!("unexpected second assembly error: {error}"));
let evidence = first.bind_simulation(successful_simulation());
let result = second.sign_after_simulation(&evidence, &[&wallet]);
assert!(result.is_err());
}
#[test]
fn missing_extra_and_duplicate_signers_are_rejected() {
let tmpwallet = wallet("payer");
let extra = wallet("extra");
let plan = transfer_plan(&tmpwallet, false, 100);
let blockhash = solana_hash::Hash::new_unique().to_string();
let unsigned = crate::executor_solana_build_legacy_transaction(&plan, blockhash.as_str())
.unwrap_or_else(|error| panic!("unexpected assembly error: {error}"));
let evidence = unsigned.bind_simulation(successful_simulation());
assert!(unsigned.clone().sign_after_simulation(&evidence, &[]).is_err());
assert!(
unsigned
.clone()
.sign_after_simulation(&evidence, &[&tmpwallet, &extra],)
.is_err()
);
assert!(unsigned.sign_after_simulation(&evidence, &[&tmpwallet, &tmpwallet],).is_err());
}
#[test]
fn durable_nonce_transaction_injects_exact_advance_and_signs() {
let payer = wallet("durable-payer");
let authority = wallet("durable-authority");
let nonce_account = crate::MdPubkey(solana_pubkey::Pubkey::new_unique().to_string());
let plan = durable_transfer_plan(&payer, &authority, &nonce_account);
assert_eq!(plan.instructions.len(), 1);
assert_eq!(plan.required_signers.len(), 2);
let nonce_state = nonce_state(&nonce_account, &authority);
let unsigned = crate::executor_solana_build_durable_nonce_transaction(&plan, &nonce_state)
.unwrap_or_else(|error| panic!("unexpected durable assembly error: {error}"));
assert_eq!(plan.instructions.len(), 1);
assert_eq!(unsigned.plan().instructions.len(), 2);
assert_eq!(unsigned.blockhash(), nonce_state.blockhash());
let authority_address =
solana_pubkey::Pubkey::from_str(authority.pubkey().to_string().as_str())
.unwrap_or_else(|error| panic!("invalid authority fixture: {error}"));
let nonce_address = solana_pubkey::Pubkey::from_str(nonce_account.0.as_str())
.unwrap_or_else(|error| panic!("invalid nonce fixture: {error}"));
let expected_advance = solana_system_interface::instruction::advance_nonce_account(
&nonce_address,
&authority_address,
);
let actual_advance = &unsigned.plan().instructions[0];
assert_eq!(
actual_advance.operation_code,
crate::EX_SOLANA_TRANSACTION_DURABLE_NONCE_ADVANCE_OPERATION
);
assert_eq!(actual_advance.program_id.0, expected_advance.program_id.to_string());
assert_eq!(actual_advance.data, expected_advance.data);
assert_eq!(actual_advance.accounts.len(), expected_advance.accounts.len());
for (actual, expected) in
actual_advance.accounts.iter().zip(expected_advance.accounts.iter())
{
assert_eq!(actual.pubkey.0, expected.pubkey.to_string());
assert_eq!(actual.is_signer, expected.is_signer);
assert_eq!(actual.is_writable, expected.is_writable);
}
assert!(solana_transaction::uses_durable_nonce(&unsigned.transaction).is_some());
let compiled_first = &unsigned.transaction.message.instructions[0];
assert_eq!(
unsigned.transaction.message.account_keys[compiled_first.program_id_index as usize],
expected_advance.program_id
);
assert_eq!(compiled_first.data, expected_advance.data);
let signer_set = unsigned
.required_signer_pubkeys()
.iter()
.cloned()
.collect::<std::collections::BTreeSet<_>>();
assert_eq!(
signer_set,
std::collections::BTreeSet::from([
payer.pubkey().to_string(),
authority.pubkey().to_string()
])
);
let evidence =
unsigned.bind_simulation(successful_nonce_simulation(&nonce_account, &authority));
assert_eq!(evidence.blockhash(), nonce_state.blockhash());
let signed = unsigned
.sign_after_simulation(&evidence, &[&payer, &authority])
.unwrap_or_else(|error| panic!("unexpected durable signing error: {error}"));
signed
.verify_signatures()
.unwrap_or_else(|error| panic!("unexpected signature verification error: {error}"));
}
#[test]
fn durable_nonce_assembly_rejects_mismatched_state_and_simulation() {
let payer = wallet("mismatch-payer");
let authority = wallet("mismatch-authority");
let wrong_authority = wallet("wrong-authority");
let nonce_account = crate::MdPubkey(solana_pubkey::Pubkey::new_unique().to_string());
let plan = durable_transfer_plan(&payer, &authority, &nonce_account);
let wrong_state = nonce_state(&nonce_account, &wrong_authority);
assert!(
crate::executor_solana_build_durable_nonce_transaction(&plan, &wrong_state).is_err()
);
let nonce_state = nonce_state(&nonce_account, &authority);
let unsigned = crate::executor_solana_build_durable_nonce_transaction(&plan, &nonce_state)
.unwrap_or_else(|error| panic!("unexpected durable assembly error: {error}"));
let latest_evidence = unsigned.bind_simulation(successful_simulation());
assert!(
unsigned
.sign_after_simulation(&latest_evidence, &[&payer, &authority],)
.is_err()
);
let mut undeclared = plan.clone();
undeclared
.required_signers
.retain(|required| return required.pubkey.0 != authority.pubkey().to_string());
assert!(
crate::executor_solana_build_durable_nonce_transaction(&undeclared, &nonce_state)
.is_err()
);
}
#[test]
fn durable_nonce_plan_requires_dedicated_assembly() {
let wallet = wallet("payer");
let mut plan = transfer_plan(&wallet, false, 100);
plan.policy.blockhash.kind = crate::ExApiExecutionBlockhashKind::DurableNonce;
plan.policy.blockhash.max_age_slots = std::option::Option::None;
plan.policy.blockhash.nonce_account =
std::option::Option::Some(crate::MdPubkey(wallet.pubkey().to_string()));
plan.policy.blockhash.nonce_authority =
std::option::Option::Some(crate::MdPubkey(wallet.pubkey().to_string()));
let nonce = solana_hash::Hash::new_unique().to_string();
assert!(crate::executor_solana_build_legacy_transaction(&plan, nonce.as_str()).is_err());
}
#[test]
fn invalid_blockhash_and_signer_contract_are_rejected() {
let wallet = wallet("payer");
let mut plan = transfer_plan(&wallet, false, 100);
assert!(crate::executor_solana_build_legacy_transaction(&plan, "invalid").is_err());
plan.required_signers.clear();
let blockhash = solana_hash::Hash::new_unique().to_string();
assert!(
crate::executor_solana_build_legacy_transaction(&plan, blockhash.as_str()).is_err()
);
}
}

View File

@@ -0,0 +1,276 @@
// file: kb-lib/src/executor/solana/transaction/nonce.rs
// version: 6
//! Durable nonce account state parsing and validation.
use std::str::FromStr; // rust-rules: trait-import
/// Validated current durable nonce account state used for transaction assembly.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ExSolanaDurableNonceAccountState {
account: crate::MdPubkey,
authority: crate::MdPubkey,
blockhash: std::string::String,
lamports_per_signature: u64,
}
impl crate::ExSolanaDurableNonceAccountState {
/// Returns the nonce account address.
pub fn account(&self) -> &crate::MdPubkey {
return &self.account;
}
/// Returns the authority that must sign the nonce advance instruction.
pub fn authority(&self) -> &crate::MdPubkey {
return &self.authority;
}
/// Returns the durable nonce value used as the transaction message blockhash.
pub fn blockhash(&self) -> &str {
return self.blockhash.as_str();
}
/// Returns the fee rate stored when the nonce was last advanced.
pub fn lamports_per_signature(&self) -> u64 {
return self.lamports_per_signature;
}
}
/// Returns the exact serialized byte length of a Solana nonce account state.
pub const fn executor_solana_durable_nonce_account_data_length() -> usize {
return solana_nonce::state::State::size();
}
/// Parses and validates a complete System Program-owned durable nonce account snapshot.
pub fn executor_solana_parse_durable_nonce_account(
account: &crate::MdPubkey,
owner: &crate::MdProgramId,
executable: bool,
space: u64,
data: &[u8],
) -> kb_core::Result<crate::ExSolanaDurableNonceAccountState> {
let account_address = match solana_pubkey::Pubkey::from_str(account.0.as_str()) {
std::result::Result::Ok(account_address) => account_address,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::new(
"execution_nonce_account_address_invalid",
format!("invalid durable nonce account address {}: {error}", account.0),
));
},
};
let owner_address = match solana_pubkey::Pubkey::from_str(owner.0.as_str()) {
std::result::Result::Ok(owner_address) => owner_address,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::new(
"execution_nonce_account_owner_invalid",
format!("invalid durable nonce account owner {}: {error}", owner.0),
));
},
};
if owner_address != solana_sdk_ids::system_program::id() {
return std::result::Result::Err(kb_core::Error::new(
"execution_nonce_account_owner_mismatch",
format!(
"durable nonce account {account_address} is owned by {owner_address} instead of the System Program"
),
));
}
if executable {
return std::result::Result::Err(kb_core::Error::new(
"execution_nonce_account_executable",
format!("durable nonce account {account_address} must not be executable"),
));
}
let expected_length = crate::executor_solana_durable_nonce_account_data_length();
if space != expected_length as u64 {
return std::result::Result::Err(kb_core::Error::new(
"execution_nonce_account_space_invalid",
format!(
"durable nonce account {account_address} reports {space} bytes instead of {expected_length}"
),
));
}
if data.len() != expected_length {
return std::result::Result::Err(kb_core::Error::new(
"execution_nonce_account_data_length_invalid",
format!(
"durable nonce account {account_address} contains {} decoded bytes instead of {expected_length}",
data.len()
),
));
}
let versions = match wincode::deserialize_exact::<solana_nonce::versions::Versions>(data) {
std::result::Result::Ok(versions) => versions,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::new(
"execution_nonce_account_state_invalid",
format!("cannot decode durable nonce account {account_address}: {error}"),
));
},
};
let state = match versions {
solana_nonce::versions::Versions::Legacy(_) => {
return std::result::Result::Err(kb_core::Error::new(
"execution_nonce_account_legacy_version",
format!(
"durable nonce account {account_address} uses the legacy nonce domain and must be upgraded before use"
),
));
},
solana_nonce::versions::Versions::Current(state) => state,
};
let initialized = match *state {
solana_nonce::state::State::Uninitialized => {
return std::result::Result::Err(kb_core::Error::new(
"execution_nonce_account_uninitialized",
format!("durable nonce account {account_address} is not initialized"),
));
},
solana_nonce::state::State::Initialized(initialized) => initialized,
};
let authority = crate::MdPubkey(initialized.authority.to_string());
let blockhash = initialized.blockhash().to_string();
let lamports_per_signature = initialized.get_lamports_per_signature();
return std::result::Result::Ok(crate::ExSolanaDurableNonceAccountState {
account: account.clone(),
authority,
blockhash,
lamports_per_signature,
});
}
#[cfg(test)]
mod tests {
fn initialized_bytes(
authority: solana_pubkey::Pubkey,
) -> (std::vec::Vec<u8>, std::string::String) {
let source_blockhash = solana_hash::Hash::new_unique();
let durable_nonce = solana_nonce::state::DurableNonce::from_blockhash(&source_blockhash);
let expected_blockhash = durable_nonce.as_hash().to_string();
let state = solana_nonce::versions::Versions::new(
solana_nonce::state::State::new_initialized(&authority, durable_nonce, 5_000),
);
let bytes = wincode::serialize(&state)
.unwrap_or_else(|error| panic!("unexpected nonce serialization error: {error}"));
return (bytes, expected_blockhash);
}
fn account(value: solana_pubkey::Pubkey) -> crate::MdPubkey {
return crate::MdPubkey(value.to_string());
}
#[test]
fn current_initialized_nonce_state_is_decoded_exactly() {
let account = account(solana_pubkey::Pubkey::new_unique());
let authority = solana_pubkey::Pubkey::new_unique();
let (bytes, expected_blockhash) = initialized_bytes(authority);
assert_eq!(bytes.len(), crate::executor_solana_durable_nonce_account_data_length());
let parsed = crate::executor_solana_parse_durable_nonce_account(
&account,
&crate::MdProgramId(solana_sdk_ids::system_program::id().to_string()),
false,
bytes.len() as u64,
bytes.as_slice(),
)
.unwrap_or_else(|error| panic!("unexpected nonce parse error: {error}"));
assert_eq!(parsed.account(), &account);
assert_eq!(parsed.authority().0, authority.to_string());
assert_eq!(parsed.blockhash(), expected_blockhash);
assert_eq!(parsed.lamports_per_signature(), 5_000);
}
#[test]
fn malformed_owner_shape_and_state_are_rejected() {
let saccount = account(solana_pubkey::Pubkey::new_unique());
let authority = solana_pubkey::Pubkey::new_unique();
let (bytes, _blockhash) = initialized_bytes(authority);
let owner = crate::MdProgramId(solana_sdk_ids::system_program::id().to_string());
assert!(
crate::executor_solana_parse_durable_nonce_account(
&saccount,
&crate::MdProgramId(solana_pubkey::Pubkey::new_unique().to_string()),
false,
bytes.len() as u64,
bytes.as_slice(),
)
.is_err()
);
assert!(
crate::executor_solana_parse_durable_nonce_account(
&saccount,
&owner,
true,
bytes.len() as u64,
bytes.as_slice(),
)
.is_err()
);
assert!(
crate::executor_solana_parse_durable_nonce_account(
&saccount,
&owner,
false,
(bytes.len() + 1) as u64,
bytes.as_slice(),
)
.is_err()
);
assert!(
crate::executor_solana_parse_durable_nonce_account(
&saccount,
&owner,
false,
bytes.len() as u64,
&bytes[..bytes.len() - 1],
)
.is_err()
);
let mut malformed = bytes.clone();
malformed[0] = 255;
assert!(
crate::executor_solana_parse_durable_nonce_account(
&saccount,
&owner,
false,
malformed.len() as u64,
malformed.as_slice(),
)
.is_err()
);
}
#[test]
fn legacy_and_uninitialized_nonce_states_are_rejected() {
let saccount = account(solana_pubkey::Pubkey::new_unique());
let owner = crate::MdProgramId(solana_sdk_ids::system_program::id().to_string());
let legacy = solana_nonce::versions::Versions::Legacy(std::boxed::Box::new(
solana_nonce::state::State::Initialized(solana_nonce::state::Data::default()),
));
let legacy_bytes = wincode::serialize(&legacy)
.unwrap_or_else(|error| panic!("unexpected legacy serialization error: {error}"));
assert!(
crate::executor_solana_parse_durable_nonce_account(
&saccount,
&owner,
false,
legacy_bytes.len() as u64,
legacy_bytes.as_slice(),
)
.is_err()
);
let uninitialized =
solana_nonce::versions::Versions::new(solana_nonce::state::State::Uninitialized);
let uninitialized_bytes = wincode::serialize(&uninitialized)
.unwrap_or_else(|error| panic!("unexpected state serialization error: {error}"));
assert!(
crate::executor_solana_parse_durable_nonce_account(
&saccount,
&owner,
false,
uninitialized_bytes.len() as u64,
uninitialized_bytes.as_slice(),
)
.is_err()
);
}
}

View File

@@ -1,5 +1,5 @@
// file: kb-lib/src/lib.rs // file: kb-lib/src/lib.rs
// version: 22 // version: 23
//! Consolidated decoder, executor, materializer and shared model library. //! Consolidated decoder, executor, materializer and shared model library.
#![warn(missing_docs)] #![warn(missing_docs)]
@@ -1067,6 +1067,14 @@ pub use self::executor::ExSolanaCoreVoteLockout;
pub use self::executor::ExSolanaCoreZkElGamalContextState; pub use self::executor::ExSolanaCoreZkElGamalContextState;
/// Exposes the official ZK ElGamal proof kinds. /// Exposes the official ZK ElGamal proof kinds.
pub use self::executor::ExSolanaCoreZkElGamalProofType; pub use self::executor::ExSolanaCoreZkElGamalProofType;
/// Validated current durable nonce account state used for transaction assembly.
pub use self::executor::ExSolanaDurableNonceAccountState;
/// Signed Solana transaction ready for RPC submission.
pub use self::executor::ExSolanaSignedTransaction;
/// Simulation result bound to the exact assembled message that was simulated.
pub use self::executor::ExSolanaSimulationEvidence;
/// Unsigned assembled Solana transaction used for fee estimation and simulation.
pub use self::executor::ExSolanaUnsignedTransaction;
/// Exposes the reserved `ExSplAccountCompressionExecutor` implementation. /// Exposes the reserved `ExSplAccountCompressionExecutor` implementation.
pub use self::executor::ExSplAccountCompressionExecutor; pub use self::executor::ExSplAccountCompressionExecutor;
/// Exposes the typed SPL ATA execution intent. /// Exposes the typed SPL ATA execution intent.
@@ -1191,6 +1199,14 @@ pub use self::executor::ExWeightedSwapStabbleExecutor;
pub use self::executor::executor_api_serialize_payload_json; pub use self::executor::executor_api_serialize_payload_json;
/// Exposes the pretty JSON payload serializer. /// Exposes the pretty JSON payload serializer.
pub use self::executor::executor_api_serialize_payload_json_pretty; pub use self::executor::executor_api_serialize_payload_json_pretty;
/// 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.
pub use self::executor::executor_solana_build_legacy_transaction;
/// Returns the exact serialized byte length of a Solana nonce account state.
pub use self::executor::executor_solana_durable_nonce_account_data_length;
/// Parses and validates a complete System Program-owned durable nonce account snapshot.
pub use self::executor::executor_solana_parse_durable_nonce_account;
/// Exposes `executor_spl_token2022_validate_confidential_proof_references`. /// Exposes `executor_spl_token2022_validate_confidential_proof_references`.
pub use self::executor::executor_spl_token2022_validate_confidential_proof_references; pub use self::executor::executor_spl_token2022_validate_confidential_proof_references;
/// Stable native and SPL administration materializer. /// Stable native and SPL administration materializer.
@@ -1756,6 +1772,10 @@ pub(crate) use self::decoder::decoder_spl_token2022_decode;
pub(crate) use self::decoder::decoder_spl_token2022_entry_for_tag; pub(crate) use self::decoder::decoder_spl_token2022_entry_for_tag;
/// Returns the first byte of one retained Token-2022 payload. /// Returns the first byte of one retained Token-2022 payload.
pub(crate) use self::decoder::decoder_spl_token2022_payload_tag; pub(crate) use self::decoder::decoder_spl_token2022_payload_tag;
/// 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.
pub(crate) use self::executor::EX_SOLANA_TRANSACTION_MAX_WIRE_BYTES;
/// Crate-root access to `EX_SPL_TOKEN_MAX_BATCH_ACCOUNTS`. /// Crate-root access to `EX_SPL_TOKEN_MAX_BATCH_ACCOUNTS`.
pub(crate) use self::executor::EX_SPL_TOKEN_MAX_BATCH_ACCOUNTS; pub(crate) use self::executor::EX_SPL_TOKEN_MAX_BATCH_ACCOUNTS;
/// Crate-root access to `EX_SPL_TOKEN_MAX_BATCH_INSTRUCTIONS`. /// Crate-root access to `EX_SPL_TOKEN_MAX_BATCH_INSTRUCTIONS`.
@@ -1772,6 +1792,8 @@ pub(crate) use self::executor::EX_SPL_TOKEN2022_MAX_TRANSFER_FEE_SOURCE_ACCOUNTS
pub(crate) use self::executor::EX_SPL_TOKEN2022_MAX_UI_AMOUNT_BYTES; pub(crate) use self::executor::EX_SPL_TOKEN2022_MAX_UI_AMOUNT_BYTES;
/// Canonical tracing target for this crate. /// Canonical tracing target for this crate.
pub(crate) use self::executor::TRACING_TARGET_EXECUTOR_SOLANA_CORE; pub(crate) use self::executor::TRACING_TARGET_EXECUTOR_SOLANA_CORE;
/// Canonical tracing target for Solana transaction assembly.
pub(crate) use self::executor::TRACING_TARGET_EXECUTOR_SOLANA_TRANSACTION;
/// Canonical SPL Memo tracing target. /// Canonical SPL Memo tracing target.
pub(crate) use self::executor::TRACING_TARGET_EXECUTOR_SPL_ATA; pub(crate) use self::executor::TRACING_TARGET_EXECUTOR_SPL_ATA;
/// Canonical tracing target for the SPL ElGamal registry executor. /// Canonical tracing target for the SPL ElGamal registry executor.