From 2a251a54469a377370760ca09ca967c56e2f06b4 Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Wed, 22 Apr 2026 16:30:02 +0700 Subject: [PATCH 1/7] chore: fix config issue --- geyser-plugin/Makefile | 15 +- geyser-plugin/README.md | 22 ++- .../configs/plugin-config.example.json | 4 + .../configs/plugin-config.example.toml | 2 - geyser-plugin/plugin-config.toml | 2 - geyser-plugin/src/config.rs | 128 ++++++++++++++++-- 6 files changed, 154 insertions(+), 19 deletions(-) create mode 100644 geyser-plugin/configs/plugin-config.example.json diff --git a/geyser-plugin/Makefile b/geyser-plugin/Makefile index 28f28a5..200928b 100644 --- a/geyser-plugin/Makefile +++ b/geyser-plugin/Makefile @@ -7,12 +7,13 @@ endif ifeq ($(UNAME_S),Darwin) PLUGIN_EXT := dylib endif -PLUGIN_PATH := target/release/libsolana_accountsdb_plugin_kafka.$(PLUGIN_EXT) +PLUGIN_PATH := ../target/release/libsolana_accountsdb_plugin_kafka.$(PLUGIN_EXT) +VALIDATOR_CONFIG ?= plugin-config.json PLUGIN_CONFIG ?= plugin-config.toml help: @echo "Available targets:" - @echo " make init-config - Create a local plugin config from configs/plugin-config.example.toml" + @echo " make init-config - Create local validator JSON + plugin TOML configs" @echo " make build-plugin - Compile the plugin (.so/.dylib)" @echo " make launch - Launch solana-test-validator with the plugin (depends on build-plugin)" @echo " make clean - Remove compiled artifacts" @@ -20,7 +21,10 @@ help: init-config: @echo "Creating $(PLUGIN_CONFIG)..." @perl -0pe 's|target/release/libsolana_accountsdb_plugin_kafka\.(?:dylib|so)|$(PLUGIN_PATH)|g' configs/plugin-config.example.toml > "$(PLUGIN_CONFIG)" + @echo "Creating $(VALIDATOR_CONFIG)..." + @perl -0pe 's|target/release/libsolana_accountsdb_plugin_kafka\.(?:dylib|so)|$(PLUGIN_PATH)|g; s|plugin-config.toml|$(PLUGIN_CONFIG)|g' configs/plugin-config.example.json > "$(VALIDATOR_CONFIG)" @echo "✓ Created $(PLUGIN_CONFIG)" + @echo "✓ Created $(VALIDATOR_CONFIG)" build-plugin: @echo "Building plugin for $(UNAME_S)..." @@ -28,12 +32,17 @@ build-plugin: launch: build-plugin @echo "Launching solana-test-validator with plugin..." + @if [ ! -f "$(VALIDATOR_CONFIG)" ]; then \ + echo "Error: $(VALIDATOR_CONFIG) not found"; \ + echo "Please create the validator config first"; \ + exit 1; \ + fi @if [ ! -f "$(PLUGIN_CONFIG)" ]; then \ echo "Error: $(PLUGIN_CONFIG) not found"; \ echo "Please create a plugin config file first"; \ exit 1; \ fi - solana-test-validator --log --reset --geyser-plugin-config "$(PLUGIN_CONFIG)" + solana-test-validator --log --reset --geyser-plugin-config "$(VALIDATOR_CONFIG)" clean: cargo clean diff --git a/geyser-plugin/README.md b/geyser-plugin/README.md index e591a0a..467584a 100644 --- a/geyser-plugin/README.md +++ b/geyser-plugin/README.md @@ -21,8 +21,14 @@ This plugin publishes confirmed Solana account updates to Kafka. ## Quick Start +```json +{ + "libpath": "../target/release/libsolana_accountsdb_plugin_kafka.so", + "config_file": "plugin-config.toml" +} +``` + ```toml -libpath = "target/release/libsolana_accountsdb_plugin_kafka.so" [kafka] bootstrap_servers = "localhost:9092" @@ -75,11 +81,20 @@ The Solana validator and this plugin must be built against matching Solana and R ## Configuration -Config is provided as TOML. +Config is split into two files: -Supported fields: +- `plugin-config.json`: validator-facing JSON wrapper passed to `--geyser-plugin-config` +- `plugin-config.toml`: plugin runtime config loaded by this crate + +The validator wrapper must contain: - `libpath`: path to the plugin shared library +- `config_file`: path to the plugin TOML config, relative to the JSON file or absolute + +The runtime config is TOML. + +Supported fields: + - `[kafka]` - `bootstrap_servers`: Kafka bootstrap broker list - `topic`: Kafka topic for wrapped account updates @@ -96,7 +111,6 @@ Supported fields: Minimal config: ```toml -libpath = "target/release/libsolana_accountsdb_plugin_kafka.so" [kafka] bootstrap_servers = "localhost:9092" diff --git a/geyser-plugin/configs/plugin-config.example.json b/geyser-plugin/configs/plugin-config.example.json new file mode 100644 index 0000000..7b83930 --- /dev/null +++ b/geyser-plugin/configs/plugin-config.example.json @@ -0,0 +1,4 @@ +{ + "libpath": "../target/release/libsolana_accountsdb_plugin_kafka.so", + "config_file": "plugin-config.toml" +} diff --git a/geyser-plugin/configs/plugin-config.example.toml b/geyser-plugin/configs/plugin-config.example.toml index 5d88d82..0d55e2a 100644 --- a/geyser-plugin/configs/plugin-config.example.toml +++ b/geyser-plugin/configs/plugin-config.example.toml @@ -1,5 +1,3 @@ -libpath = "target/release/libsolana_accountsdb_plugin_kafka.so" - [kafka] bootstrap_servers = "localhost:9092" topic = "solana.testnet.account_updates" diff --git a/geyser-plugin/plugin-config.toml b/geyser-plugin/plugin-config.toml index 5d88d82..0d55e2a 100644 --- a/geyser-plugin/plugin-config.toml +++ b/geyser-plugin/plugin-config.toml @@ -1,5 +1,3 @@ -libpath = "target/release/libsolana_accountsdb_plugin_kafka.so" - [kafka] bootstrap_servers = "localhost:9092" topic = "solana.testnet.account_updates" diff --git a/geyser-plugin/src/config.rs b/geyser-plugin/src/config.rs index 9582ba2..8a59896 100644 --- a/geyser-plugin/src/config.rs +++ b/geyser-plugin/src/config.rs @@ -19,7 +19,11 @@ use { reqwest::Url, serde::Deserialize, std::{ - collections::BTreeMap, fs::File, io::Read, net::SocketAddr, path::Path, + collections::BTreeMap, + fs::File, + io::Read, + net::SocketAddr, + path::{Path, PathBuf}, }, }; @@ -28,7 +32,8 @@ use { #[serde(deny_unknown_fields)] pub struct Config { #[allow(dead_code)] - libpath: String, + #[serde(default)] + libpath: Option, /// Kafka config. pub kafka: KafkaConfig, @@ -70,6 +75,14 @@ pub struct PluginConfig { pub metrics: bool, } +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct ValidatorConfig { + #[allow(dead_code)] + libpath: String, + config_file: PathBuf, +} + fn default_shutdown_timeout_ms() -> u64 { 30_000 } @@ -81,7 +94,7 @@ fn default_ksql_table() -> String { impl Default for Config { fn default() -> Self { Self { - libpath: "".to_owned(), + libpath: None, kafka: KafkaConfig { bootstrap_servers: String::new(), topic: String::new(), @@ -111,12 +124,24 @@ impl Default for KsqlConfig { } impl Config { - /// Read plugin from TOML file. + /// Read plugin config from either a validator JSON wrapper or a TOML runtime config. pub fn read_from>(config_path: P) -> PluginResult { - let mut file = File::open(config_path)?; - let mut contents = String::new(); - file.read_to_string(&mut contents)?; - let mut this: Self = toml::from_str(&contents).map_err(|e| { + let config_path = config_path.as_ref(); + let contents = read_to_string(config_path)?; + let runtime_path = + match serde_json::from_str::(&contents) { + Ok(wrapper) => resolve_runtime_config_path( + config_path, + &wrapper.config_file, + ), + Err(_) => config_path.to_path_buf(), + }; + let runtime_contents = if runtime_path == config_path { + contents + } else { + read_to_string(&runtime_path)? + }; + let mut this: Self = toml::from_str(&runtime_contents).map_err(|e| { GeyserPluginError::ConfigFileReadError { msg: e.to_string() } })?; this.fill_defaults(); @@ -209,9 +234,34 @@ impl Config { } } +fn read_to_string(path: &Path) -> PluginResult { + let mut file = File::open(path)?; + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + Ok(contents) +} + +fn resolve_runtime_config_path( + wrapper_path: &Path, + runtime_path: &Path, +) -> PathBuf { + if runtime_path.is_absolute() { + runtime_path.to_path_buf() + } else { + wrapper_path + .parent() + .unwrap_or_else(|| Path::new(".")) + .join(runtime_path) + } +} + #[cfg(test)] mod tests { use super::Config; + use std::{ + fs, + time::{SystemTime, UNIX_EPOCH}, + }; fn parse_config(toml: &str) -> Result { let mut config: Config = @@ -242,6 +292,68 @@ admin = "127.0.0.1:8080" assert_eq!(config.plugin.local_rpc_url, "http://127.0.0.1:8899"); } + #[test] + fn test_parses_valid_minimal_config_without_libpath() { + let config = parse_config( + r#" +[kafka] +bootstrap_servers = "localhost:9092" +topic = "solana.testnet.account_updates" + +[plugin] +local_rpc_url = "http://127.0.0.1:8899" +admin = "127.0.0.1:8080" +"#, + ) + .unwrap(); + + assert_eq!(config.kafka.topic, "solana.testnet.account_updates"); + assert_eq!(config.plugin.local_rpc_url, "http://127.0.0.1:8899"); + } + + #[test] + fn test_reads_runtime_toml_via_validator_json_wrapper() { + let base = std::env::temp_dir().join(format!( + "geyser-plugin-config-test-{}", + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + fs::create_dir_all(&base).unwrap(); + + let runtime_path = base.join("runtime.toml"); + fs::write( + &runtime_path, + r#" +[kafka] +bootstrap_servers = "localhost:9092" +topic = "solana.testnet.account_updates" + +[plugin] +local_rpc_url = "http://127.0.0.1:8899" +admin = "127.0.0.1:8080" +"#, + ) + .unwrap(); + + let wrapper_path = base.join("plugin-config.json"); + fs::write( + &wrapper_path, + r#"{ + "libpath": "target/release/libsolana_accountsdb_plugin_kafka.so", + "config_file": "runtime.toml" +}"#, + ) + .unwrap(); + + let config = Config::read_from(&wrapper_path).unwrap(); + assert_eq!(config.kafka.topic, "solana.testnet.account_updates"); + assert_eq!(config.plugin.admin.port(), 8080); + + fs::remove_dir_all(&base).unwrap(); + } + #[test] fn test_rejects_missing_admin() { let error = parse_config( From 12a19f8f0d442ee51ef886212a0065cc5ad9e8be Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Wed, 22 Apr 2026 16:30:11 +0700 Subject: [PATCH 2/7] chore: pin solana versions --- Cargo.lock | 92 ++++++++++++++++++++-------------------- geyser-plugin/Cargo.toml | 10 ++--- 2 files changed, 51 insertions(+), 51 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f1faffb..5f766da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -69,9 +69,9 @@ dependencies = [ [[package]] name = "agave-feature-set" -version = "4.0.0-beta.7" +version = "4.0.0-beta.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b16372df9ec6577a8e4140c85aa8b743d99945cbeebbc0d7b739136a4e601a4" +checksum = "608865a64ed3ce4b4822c8e5facef41b80392fe664ba13293e8d54ca76a074f6" dependencies = [ "ahash", "solana-epoch-schedule 3.1.0", @@ -84,24 +84,24 @@ dependencies = [ [[package]] name = "agave-geyser-plugin-interface" -version = "4.0.0-beta.7" +version = "4.0.0-beta.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6ba35e7efa263778408f5087daf4b389dd53ab081c38828778ce0e31aeafa2" +checksum = "4dea6af8d4fc8663772b03d77658e31c9c887adbecf1d818a1f332dcfce2a7e5" dependencies = [ "log", "solana-clock 3.0.1", "solana-hash 4.3.0", "solana-signature 3.4.0", "solana-transaction 3.1.0", - "solana-transaction-status 4.0.0-beta.7", + "solana-transaction-status 4.0.0-beta.4", "thiserror 2.0.18", ] [[package]] name = "agave-logger" -version = "4.0.0-beta.7" +version = "4.0.0-beta.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "118847932f6942d9f22407411ceabacaa72cf0f85ce806c277f885fbacddfa18" +checksum = "61fe6a8dc3d70d8353d0a9c471d39c7bdca93725211906deb7a7f80f2b6a5a8b" dependencies = [ "env_logger", "libc", @@ -146,11 +146,11 @@ dependencies = [ [[package]] name = "agave-reserved-account-keys" -version = "4.0.0-beta.7" +version = "4.0.0-beta.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b2836768518bebd3956b52f7ec7c9284b7830130c18e8652f1a43dc04fee1e" +checksum = "5e94f7811efb21ec459af4506482c6d1e8f0eb444a480cd18ee341ce54c1774b" dependencies = [ - "agave-feature-set 4.0.0-beta.7", + "agave-feature-set 4.0.0-beta.4", "solana-pubkey 4.1.0", "solana-sdk-ids 3.1.0", ] @@ -4105,9 +4105,9 @@ dependencies = [ [[package]] name = "solana-account-decoder" -version = "4.0.0-beta.7" +version = "4.0.0-beta.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cef67445b00fa0d3ab67ddd1397012d961cf74d1cb47224ba1375d351991181" +checksum = "d4d5c031368143d0a398421064a605aa3b56f4f9f5b8680d46219602bbb1ba33" dependencies = [ "Inflector", "base64 0.22.1", @@ -4117,7 +4117,7 @@ dependencies = [ "serde", "serde_json", "solana-account 3.4.0", - "solana-account-decoder-client-types 4.0.0-beta.7", + "solana-account-decoder-client-types 4.0.0-beta.4", "solana-address-lookup-table-interface 3.0.1", "solana-clock 3.0.1", "solana-config-interface", @@ -4163,9 +4163,9 @@ dependencies = [ [[package]] name = "solana-account-decoder-client-types" -version = "4.0.0-beta.7" +version = "4.0.0-beta.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da42c070e1d8a268c9ab746352ad1883c81af2529b3d11cb66d8484a746bd9d8" +checksum = "0093cd074e0d32d2ca2100a48480e7e90bd23323d79790da0d2cab8bdbeb59d2" dependencies = [ "base64 0.22.1", "bs58", @@ -4230,8 +4230,8 @@ dependencies = [ "solana-message 3.1.0", "solana-pubkey 4.1.0", "solana-rpc-client", - "solana-transaction-status 4.0.0-beta.7", - "solana-transaction-status-client-types 4.0.0-beta.7", + "solana-transaction-status 4.0.0-beta.4", + "solana-transaction-status-client-types 4.0.0-beta.4", "tokio", "toml 0.9.12+spec-1.1.0", "tower 0.5.3", @@ -5475,9 +5475,9 @@ dependencies = [ [[package]] name = "solana-rpc-client" -version = "4.0.0-beta.7" +version = "4.0.0-beta.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1085efc9679ad7eb357b0bc711a16ff106a2f031bf7501d7279adf360fd6928f" +checksum = "4d7803e5a9f7d1eb427888f3ff0bd73cb1fdc3d70f140507d2b11fe9ecc07b6c" dependencies = [ "async-trait", "base64 0.22.1", @@ -5491,8 +5491,8 @@ dependencies = [ "serde", "serde_json", "solana-account 3.4.0", - "solana-account-decoder 4.0.0-beta.7", - "solana-account-decoder-client-types 4.0.0-beta.7", + "solana-account-decoder 4.0.0-beta.4", + "solana-account-decoder-client-types 4.0.0-beta.4", "solana-clock 3.0.1", "solana-commitment-config 3.1.1", "solana-epoch-info", @@ -5506,7 +5506,7 @@ dependencies = [ "solana-signature 3.4.0", "solana-transaction 3.1.0", "solana-transaction-error 3.2.0", - "solana-transaction-status-client-types 4.0.0-beta.7", + "solana-transaction-status-client-types 4.0.0-beta.4", "solana-version", "solana-vote-interface 5.1.1", "tokio", @@ -5514,9 +5514,9 @@ dependencies = [ [[package]] name = "solana-rpc-client-api" -version = "4.0.0-beta.7" +version = "4.0.0-beta.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7a12b9801d7bca997a8bc0494df224eafee830f6313cc65100c76bb5df4c46d" +checksum = "5b5dbebaf8056626936ffe96c15a4cd5580790728ed16dcf2db27f90a5d8b480" dependencies = [ "anyhow", "jsonrpc-core", @@ -5524,20 +5524,20 @@ dependencies = [ "reqwest-middleware", "serde", "serde_json", - "solana-account-decoder-client-types 4.0.0-beta.7", + "solana-account-decoder-client-types 4.0.0-beta.4", "solana-clock 3.0.1", "solana-rpc-client-types", "solana-signer 3.0.0", "solana-transaction-error 3.2.0", - "solana-transaction-status-client-types 4.0.0-beta.7", + "solana-transaction-status-client-types 4.0.0-beta.4", "thiserror 2.0.18", ] [[package]] name = "solana-rpc-client-types" -version = "4.0.0-beta.7" +version = "4.0.0-beta.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3196fe76562ac3a68deef16914074c60132321b681dbb33f5ea8d5adb1fe0b4d" +checksum = "6de480101f33103d069ab481fc674e72d7c5f30dc289ed822f1bdfa05700c4bd" dependencies = [ "base64 0.22.1", "bs58", @@ -5545,7 +5545,7 @@ dependencies = [ "serde", "serde_json", "solana-account 3.4.0", - "solana-account-decoder-client-types 4.0.0-beta.7", + "solana-account-decoder-client-types 4.0.0-beta.4", "solana-address 2.6.0", "solana-clock 3.0.1", "solana-commitment-config 3.1.1", @@ -5554,7 +5554,7 @@ dependencies = [ "solana-reward-info 5.0.0", "solana-transaction 3.1.0", "solana-transaction-error 3.2.0", - "solana-transaction-status-client-types 4.0.0-beta.7", + "solana-transaction-status-client-types 4.0.0-beta.4", "solana-version", "spl-generic-token", "thiserror 2.0.18", @@ -5974,9 +5974,9 @@ dependencies = [ [[package]] name = "solana-svm-feature-set" -version = "4.0.0-beta.7" +version = "4.0.0-beta.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2770de6ab3b2f74b1942128fe6e32f343f4df23b116e9b94bd6860b753d0551b" +checksum = "af8833dd3a3351278a50bb8f1de70636d126ab08ee708ac0c9977c4da25a9ed6" [[package]] name = "solana-system-interface" @@ -6185,9 +6185,9 @@ dependencies = [ [[package]] name = "solana-transaction-context" -version = "4.0.0-beta.7" +version = "4.0.0-beta.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e081c70560c26b67c1e8451c71c08dd70a44f288aa1d006cd1554e9a834b1a72" +checksum = "6c0bdeb5c8aba3fc2c3db7322b951db9810f772ecb28d03c48d70ea59d9278d5" dependencies = [ "bincode", "serde", @@ -6268,12 +6268,12 @@ dependencies = [ [[package]] name = "solana-transaction-status" -version = "4.0.0-beta.7" +version = "4.0.0-beta.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f4e6ced5772331fdd10d793ed7f487aeeb4d6217c1774ca21b19ccba8e4331" +checksum = "c81f16580c918e55abf199aba4b7329f4d7221c7cee00d24a6b4b12ca8d13c62" dependencies = [ "Inflector", - "agave-reserved-account-keys 4.0.0-beta.7", + "agave-reserved-account-keys 4.0.0-beta.4", "base64 0.22.1", "bincode", "borsh 1.6.1", @@ -6281,7 +6281,7 @@ dependencies = [ "log", "serde", "serde_json", - "solana-account-decoder 4.0.0-beta.7", + "solana-account-decoder 4.0.0-beta.4", "solana-address-lookup-table-interface 3.0.1", "solana-clock 3.0.1", "solana-hash 4.3.0", @@ -6298,7 +6298,7 @@ dependencies = [ "solana-system-interface 3.2.0", "solana-transaction 3.1.0", "solana-transaction-error 3.2.0", - "solana-transaction-status-client-types 4.0.0-beta.7", + "solana-transaction-status-client-types 4.0.0-beta.4", "solana-vote-interface 5.1.1", "spl-associated-token-account-interface", "spl-memo-interface", @@ -6334,16 +6334,16 @@ dependencies = [ [[package]] name = "solana-transaction-status-client-types" -version = "4.0.0-beta.7" +version = "4.0.0-beta.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bb387b44eec1887694ac2264e35951f0c0763014b6593ae17f13cb3088fa2cc" +checksum = "e4f4346057212353c2b2383791a10f575930b430ab08d8740fcca1c0ebd7f68f" dependencies = [ "base64 0.22.1", "bincode", "bs58", "serde", "serde_json", - "solana-account-decoder-client-types 4.0.0-beta.7", + "solana-account-decoder-client-types 4.0.0-beta.4", "solana-commitment-config 3.1.1", "solana-instruction 3.4.0", "solana-message 3.1.0", @@ -6351,7 +6351,7 @@ dependencies = [ "solana-reward-info 5.0.0", "solana-signature 3.4.0", "solana-transaction 3.1.0", - "solana-transaction-context 4.0.0-beta.7", + "solana-transaction-context 4.0.0-beta.4", "solana-transaction-error 3.2.0", "thiserror 2.0.18", ] @@ -6368,11 +6368,11 @@ dependencies = [ [[package]] name = "solana-version" -version = "4.0.0-beta.7" +version = "4.0.0-beta.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5316d63c5a8dfce421d49f5c2234e568f8fe34e1a372d6f55e397f11c623b475" +checksum = "de5afbb87e68264676dc66523429b8145a68418fba7971fa668e5a4df820b8ae" dependencies = [ - "agave-feature-set 4.0.0-beta.7", + "agave-feature-set 4.0.0-beta.4", "rand 0.9.4", "semver", "serde", diff --git a/geyser-plugin/Cargo.toml b/geyser-plugin/Cargo.toml index df5c8ef..3e49354 100644 --- a/geyser-plugin/Cargo.toml +++ b/geyser-plugin/Cargo.toml @@ -14,15 +14,15 @@ categories = ["cryptography::cryptocurrencies", "database", "network-programming crate-type = ["cdylib", "rlib"] [dependencies] -agave-geyser-plugin-interface = { version = "4.0.0-beta.4" } -agave-logger = { version = "4.0.0-beta.4", features = ["agave-unstable-api"] } +agave-geyser-plugin-interface = { version = "=4.0.0-beta.4" } +agave-logger = { version = "=4.0.0-beta.4", features = ["agave-unstable-api"] } solana-message = { version = "~3.1" } solana-account = "~3.4" solana-commitment-config = "3.1.1" solana-pubkey = { version = "~4.1" } -solana-rpc-client = { version = "4.0.0-beta.4", default-features = false } -solana-transaction-status = { version = "4.0.0-beta" } -solana-transaction-status-client-types = { version = "4.0.0-beta" } +solana-rpc-client = { version = "=4.0.0-beta.4", default-features = false } +solana-transaction-status = { version = "=4.0.0-beta.4" } +solana-transaction-status-client-types = { version = "=4.0.0-beta.4" } bytes = "1.11.1" base64 = "0.22.1" From d40859cebfb084bf94fc338efc55a1e0b0deec16 Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Wed, 22 Apr 2026 16:40:13 +0700 Subject: [PATCH 3/7] chore: better handling of unexpected ksql responses --- geyser-plugin/src/account_update_publisher.rs | 4 ++-- geyser-plugin/src/ksql.rs | 16 +++++++++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/geyser-plugin/src/account_update_publisher.rs b/geyser-plugin/src/account_update_publisher.rs index d82ae13..6d08095 100644 --- a/geyser-plugin/src/account_update_publisher.rs +++ b/geyser-plugin/src/account_update_publisher.rs @@ -139,8 +139,8 @@ fn log_ignore_account_update(pubkey: &[u8]) { ); return; } - if log_enabled!(::log::Level::Debug) { - debug!("Ignoring update for account key bytes: {:?}", pubkey); + if log_enabled!(::log::Level::Trace) { + trace!("Ignoring update for account key bytes: {:?}", pubkey); } } diff --git a/geyser-plugin/src/ksql.rs b/geyser-plugin/src/ksql.rs index 865cd46..fc9166d 100644 --- a/geyser-plugin/src/ksql.rs +++ b/geyser-plugin/src/ksql.rs @@ -32,7 +32,7 @@ impl KsqlPubkeyRestoreClient { } pub(crate) fn fetch_pubkeys(&self) -> io::Result> { - let sql = format!("SELECT \"PUBKEY\" FROM \"{}\";", self.table); + let sql = format!("SELECT PUBKEY FROM {};", self.table); let query_url = format!("{}/query-stream", self.base_url); debug!( "Querying ksql for startup restore, url={}, sql={}", @@ -49,12 +49,18 @@ impl KsqlPubkeyRestoreClient { .send() .map_err(|error| { io::Error::other(format!("failed to query ksqlDB: {error}")) - })? - .error_for_status() - .map_err(|error| { - io::Error::other(format!("ksqlDB query failed: {error}")) })?; + let status = response.status(); + if !status.is_success() { + let body = response.text().unwrap_or_else(|error| { + format!("") + }); + return Err(io::Error::other(format!( + "ksqlDB query failed with HTTP status {status}: {body}" + ))); + } + let reader = BufReader::new(response); let pubkeys = parse_pubkeys_stream(reader)?; debug!( From ac438ccd5c326bbd1b27b26543f1f175b74776b6 Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Mon, 27 Apr 2026 16:10:08 +0700 Subject: [PATCH 4/7] fix: prevent init-config path double-prefixing Amp-Thread-ID: https://ampcode.com/threads/T-019dce32-32f7-7779-af5a-cf80c3f4b9b2 Co-authored-by: Amp --- geyser-plugin/Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/geyser-plugin/Makefile b/geyser-plugin/Makefile index 200928b..f7adabe 100644 --- a/geyser-plugin/Makefile +++ b/geyser-plugin/Makefile @@ -20,9 +20,9 @@ help: init-config: @echo "Creating $(PLUGIN_CONFIG)..." - @perl -0pe 's|target/release/libsolana_accountsdb_plugin_kafka\.(?:dylib|so)|$(PLUGIN_PATH)|g' configs/plugin-config.example.toml > "$(PLUGIN_CONFIG)" + @perl -0pe 's#(? "$(PLUGIN_CONFIG)" @echo "Creating $(VALIDATOR_CONFIG)..." - @perl -0pe 's|target/release/libsolana_accountsdb_plugin_kafka\.(?:dylib|so)|$(PLUGIN_PATH)|g; s|plugin-config.toml|$(PLUGIN_CONFIG)|g' configs/plugin-config.example.json > "$(VALIDATOR_CONFIG)" + @perl -0pe 's#(? "$(VALIDATOR_CONFIG)" @echo "✓ Created $(PLUGIN_CONFIG)" @echo "✓ Created $(VALIDATOR_CONFIG)" From a40f8a9abe764b0e7b4c8c00bd1d0b024ff2ee53 Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Mon, 27 Apr 2026 16:11:48 +0700 Subject: [PATCH 5/7] fix: accept config_file from validator JSON Amp-Thread-ID: https://ampcode.com/threads/T-019dce34-6384-7068-938f-012ea8571e67 Co-authored-by: Amp --- geyser-plugin/Makefile | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/geyser-plugin/Makefile b/geyser-plugin/Makefile index f7adabe..4fab2f0 100644 --- a/geyser-plugin/Makefile +++ b/geyser-plugin/Makefile @@ -38,9 +38,13 @@ launch: build-plugin exit 1; \ fi @if [ ! -f "$(PLUGIN_CONFIG)" ]; then \ - echo "Error: $(PLUGIN_CONFIG) not found"; \ - echo "Please create a plugin config file first"; \ - exit 1; \ + cf=$$(perl -0ne 'print $$1 if /"config_file"\s*:\s*"([^"]+)"/' "$(VALIDATOR_CONFIG)"); \ + if [ -z "$$cf" ] || [ ! -f "$$cf" ]; then \ + echo "Error: plugin config not found"; \ + echo " Tried $(PLUGIN_CONFIG) and config_file from $(VALIDATOR_CONFIG) ($$cf)"; \ + echo "Please create a plugin config file first"; \ + exit 1; \ + fi; \ fi solana-test-validator --log --reset --geyser-plugin-config "$(VALIDATOR_CONFIG)" From 4a0cb1156582feb456814f01de6a9afe0a5aea0a Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Mon, 27 Apr 2026 16:14:21 +0700 Subject: [PATCH 6/7] fix: propagate JSON parse error in config wrapper When read_from receives a JSON-looking input (extension is .json or contents start with { or [), surface the original serde_json parse error instead of silently falling back to TOML parsing, which would produce a misleading TOML error for a malformed validator wrapper. Amp-Thread-ID: https://ampcode.com/threads/T-019dce36-f46e-762c-b867-c6a2743093b0 Co-authored-by: Amp --- geyser-plugin/src/config.rs | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/geyser-plugin/src/config.rs b/geyser-plugin/src/config.rs index 8a59896..d713b7b 100644 --- a/geyser-plugin/src/config.rs +++ b/geyser-plugin/src/config.rs @@ -134,16 +134,32 @@ impl Config { config_path, &wrapper.config_file, ), - Err(_) => config_path.to_path_buf(), + Err(error) => { + let looks_like_json = config_path + .extension() + .and_then(|ext| ext.to_str()) + .is_some_and(|ext| ext.eq_ignore_ascii_case("json")) + || matches!( + contents.trim_start().as_bytes().first(), + Some(b'{') | Some(b'[') + ); + if looks_like_json { + return Err(GeyserPluginError::ConfigFileReadError { + msg: error.to_string(), + }); + } + config_path.to_path_buf() + } }; let runtime_contents = if runtime_path == config_path { contents } else { read_to_string(&runtime_path)? }; - let mut this: Self = toml::from_str(&runtime_contents).map_err(|e| { - GeyserPluginError::ConfigFileReadError { msg: e.to_string() } - })?; + let mut this: Self = + toml::from_str(&runtime_contents).map_err(|e| { + GeyserPluginError::ConfigFileReadError { msg: e.to_string() } + })?; this.fill_defaults(); this.validate()?; Ok(this) From 0acf2fe0daec47f2fce50d2501d83cee65675764 Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Mon, 27 Apr 2026 16:16:10 +0700 Subject: [PATCH 7/7] fix: validate ksql table identifier before SQL interpolation The fetch_pubkeys query previously interpolated self.table directly into the SELECT statement, which would allow SQL injection or invalid identifiers if the configured table name contained unexpected characters. Add a validate_ksql_identifier helper that requires the identifier to start with an ASCII letter or '_' and contain only ASCII alphanumeric characters or '_', call it before formatting the query, and return an io::Error on failure. Also adds tests covering the validator's accept and reject paths. Amp-Thread-ID: https://ampcode.com/threads/T-019dce38-5a38-770a-8f3b-a0773875c151 Co-authored-by: Amp --- geyser-plugin/src/ksql.rs | 62 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/geyser-plugin/src/ksql.rs b/geyser-plugin/src/ksql.rs index fc9166d..e595a19 100644 --- a/geyser-plugin/src/ksql.rs +++ b/geyser-plugin/src/ksql.rs @@ -32,7 +32,8 @@ impl KsqlPubkeyRestoreClient { } pub(crate) fn fetch_pubkeys(&self) -> io::Result> { - let sql = format!("SELECT PUBKEY FROM {};", self.table); + let table = validate_ksql_identifier(&self.table)?; + let sql = format!("SELECT PUBKEY FROM {table};"); let query_url = format!("{}/query-stream", self.base_url); debug!( "Querying ksql for startup restore, url={}, sql={}", @@ -71,6 +72,30 @@ impl KsqlPubkeyRestoreClient { } } +/// Validates that `identifier` is a safe ksqlDB identifier suitable for +/// direct interpolation into a SQL statement. The identifier must start with +/// an ASCII letter or `_` and may otherwise contain only ASCII alphanumeric +/// characters or `_`. +pub(crate) fn validate_ksql_identifier(identifier: &str) -> io::Result<&str> { + let mut chars = identifier.chars(); + let first = chars + .next() + .ok_or_else(|| io::Error::other("ksql identifier must not be empty"))?; + if !(first.is_ascii_alphabetic() || first == '_') { + return Err(io::Error::other(format!( + "invalid ksql identifier `{identifier}`: must start with an ASCII letter or `_`" + ))); + } + for c in chars { + if !(c.is_ascii_alphanumeric() || c == '_') { + return Err(io::Error::other(format!( + "invalid ksql identifier `{identifier}`: only ASCII alphanumeric characters and `_` are allowed" + ))); + } + } + Ok(identifier) +} + pub(crate) fn parse_pubkeys_stream( reader: impl BufRead, ) -> io::Result> { @@ -147,7 +172,7 @@ pub(crate) fn parse_pubkeys_stream( #[cfg(test)] mod tests { - use super::parse_pubkeys_stream; + use super::{parse_pubkeys_stream, validate_ksql_identifier}; fn pubkey(byte: u8) -> [u8; 32] { [byte; 32] @@ -223,4 +248,37 @@ mod tests { assert!(error.contains("expected 32 decoded PUBKEY bytes")); } + + #[test] + fn test_validates_simple_identifier() { + assert_eq!(validate_ksql_identifier("accounts").unwrap(), "accounts"); + assert_eq!(validate_ksql_identifier("_x").unwrap(), "_x"); + assert_eq!(validate_ksql_identifier("A1_b2").unwrap(), "A1_b2"); + } + + #[test] + fn test_rejects_empty_identifier() { + let error = validate_ksql_identifier("").unwrap_err().to_string(); + assert!(error.contains("must not be empty")); + } + + #[test] + fn test_rejects_identifier_starting_with_digit() { + let error = validate_ksql_identifier("1bad").unwrap_err().to_string(); + assert!(error.contains("must start with an ASCII letter")); + } + + #[test] + fn test_rejects_identifier_with_invalid_characters() { + let error = validate_ksql_identifier("accounts; DROP TABLE x") + .unwrap_err() + .to_string(); + assert!(error.contains("only ASCII alphanumeric")); + } + + #[test] + fn test_rejects_identifier_with_quote() { + let error = validate_ksql_identifier("a\"b").unwrap_err().to_string(); + assert!(error.contains("only ASCII alphanumeric")); + } }