Kora is a Rust workspace with three crates:
kora-libholds the core logic and the RPC server,kora-clibuilds thekorabinary operators run, andkora-deployis a specialized program deployment tool, versioned separately. Everything below lives incrates/lib/srcunless noted.
The request lifecycle
The diagram below identifies the general flow of a transaction through Kora. Understanding the call path is important to configuring a Kora node and adding new features.
Where each stage lives
| Stage in the diagram | Source | Entry point |
|---|---|---|
| Gateway | rpc_server/server.rs | run_rpc_server |
| Dispatch | rpc_server/rpc.rs | KoraRpc |
| Per-method logic | rpc_server/method/ | one file per JSON-RPC method |
| Validation | validator/transaction_validator.rs | TransactionValidator |
| Plugins | plugin/mod.rs | TransactionPluginRunner |
| Signing | signer/ | backed by the solana-keychain crate |
Two more files to know: config.rs defines the entire kora.toml schema, and
state.rs holds the deserialized config in process-global state.
Reading the RPC server
rpc.rs is a thin dispatch layer, one method per JSON-RPC endpoint, and the
fastest way to see the whole API. Each method's logic sits in its own file under
rpc_server/method/. Read sign_transaction.rs end to end; the other signing
methods are variations on it.
Reading the validator
transaction_validator.rs and transaction/instruction_util.rs hold logic for
validating transactions and instructions. Additionally, the fee-payer-policy
safety property tests, which live one file per gated program under
validator/transaction_validator/fee_payer_policy_props/.
Conventions worth knowing
Default validation. Every flag under [validation.fee_payer_policy]
defaults to false, and the policy structs carry #[serde(default)]. That
pairing is deliberate: adding a newly gated instruction must not break an
existing operator's config, and it must land as a denial rather than an
accidental allowance. Preserve both when you add a flag.
Validate the resolved transaction, never the raw one. The raw
VersionedTransaction exposes only static account keys, so validating or
pricing a V0 transaction from it silently skips lookup-table accounts and
under-charges. VersionedTransactionResolved::from_transaction resolves those
addresses up front, which is why every downstream stage takes the resolved type.
Adding a transaction plugin
Plugins implement the TransactionPlugin trait in plugin/mod.rs. It has two
methods: validate runs per transaction during the signing flows, and
validate_config runs once at startup and returns errors and warnings for the
operator. A PluginExecutionContext tells the plugin which of the four signing
methods invoked it.
TransactionPluginRunner::from_config matches on TransactionPluginType,
defined in config.rs. A new built-in plugin is a new variant there plus a new
match arm in the runner. The two plugins that ship today are gas_swap and
deploy_authority.
Testing
Kora keeps configuration in process-global state, so tests/ declares one
[[test]] target per phase, and each target boots its own embedded surfnet plus
Kora node, seeded from scratch. One config per binary: a new phase needs a new
target, not a new module in an existing one.
cargo test -p tests --test <target>
Everything else is wired through the justfile. Read that rather than
reconstructing commands by hand.
Is this page helpful?