Skip to content

Commit 3661d53

Browse files
kariyclaude
andauthored
feat(grpc): add gRPC server with Starknet API parity (#408)
Add gRPC server alongside JSON-RPC for high-performance RPC access - exposes Starknet read, write, and trace APIs. --------- Co-authored-by: Ammar Arif <kariy@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 5389c32 commit 3661d53

38 files changed

+4252
-400
lines changed

.tool-versions

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
scarb 2.8.2
1+
scarb 2.15.0
22
katana 1.7.0

Cargo.lock

Lines changed: 32 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ katana-gateway-server = { path = "crates/gateway/gateway-server" }
8888
katana-fork = { path = "crates/storage/fork" }
8989
katana-gas-price-oracle = { path = "crates/oracle/gas" }
9090
katana-genesis = { path = "crates/genesis" }
91+
katana-grpc = { path = "crates/grpc" }
9192
katana-messaging = { path = "crates/messaging" }
9293
katana-metrics = { path = "crates/metrics" }
9394
katana-node = { path = "crates/node" }
@@ -194,6 +195,7 @@ tower-service = "0.3"
194195
prost = "0.12"
195196
tonic = { version = "0.11", features = [ "gzip", "tls", "tls-roots", "tls-webpki-roots" ] }
196197
tonic-build = "0.11"
198+
tonic-reflection = "0.11"
197199

198200
# benchmark
199201
criterion = "0.5.1"

crates/cli/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ cartridge = [
4242
"katana-node/cartridge",
4343
"katana-rpc-server/cartridge",
4444
]
45-
default = ["cartridge", "server", "tee"]
45+
grpc = [ "katana-node/grpc" ]
46+
default = ["cartridge", "server", "tee", "grpc"]
4647
explorer = ["katana-node/explorer", "katana-utils/explorer"]
4748
native = ["katana-node/native"]
4849
server = []

crates/cli/src/args.rs

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ use katana_node::config::execution::ExecutionConfig;
2020
use katana_node::config::fork::ForkingConfig;
2121
#[cfg(feature = "server")]
2222
use katana_node::config::gateway::GatewayConfig;
23+
#[cfg(all(feature = "server", feature = "grpc"))]
24+
use katana_node::config::grpc::GrpcConfig;
2325
use katana_node::config::metrics::MetricsConfig;
2426
#[cfg(feature = "cartridge")]
2527
use katana_node::config::paymaster::PaymasterConfig;
@@ -135,6 +137,10 @@ pub struct SequencerNodeArgs {
135137
#[cfg(feature = "tee")]
136138
#[command(flatten)]
137139
pub tee: TeeOptions,
140+
141+
#[cfg(all(feature = "server", feature = "grpc"))]
142+
#[command(flatten)]
143+
pub grpc: GrpcOptions,
138144
}
139145

140146
impl SequencerNodeArgs {
@@ -209,6 +215,8 @@ impl SequencerNodeArgs {
209215
let (chain, cs_messaging) = self.chain_spec()?;
210216
let metrics = self.metrics_config();
211217
let gateway = self.gateway_config();
218+
#[cfg(all(feature = "server", feature = "grpc"))]
219+
let grpc = self.grpc_config();
212220
let forking = self.forking_config()?;
213221
let execution = self.execution_config();
214222
let sequencing = self.sequencer_config();
@@ -222,6 +230,8 @@ impl SequencerNodeArgs {
222230
db,
223231
dev,
224232
rpc,
233+
#[cfg(feature = "grpc")]
234+
grpc,
225235
chain,
226236
metrics,
227237
gateway,
@@ -448,6 +458,21 @@ impl SequencerNodeArgs {
448458
None
449459
}
450460

461+
#[cfg(all(feature = "server", feature = "grpc"))]
462+
fn grpc_config(&self) -> Option<GrpcConfig> {
463+
if self.grpc.grpc_enable {
464+
use std::time::Duration;
465+
466+
Some(GrpcConfig {
467+
addr: self.grpc.grpc_addr,
468+
port: self.grpc.grpc_port,
469+
timeout: self.grpc.grpc_timeout.map(Duration::from_secs),
470+
})
471+
} else {
472+
None
473+
}
474+
}
475+
451476
#[cfg(feature = "cartridge")]
452477
fn cartridge_config(&self) -> Option<PaymasterConfig> {
453478
if self.cartridge.paymaster {
@@ -508,6 +533,11 @@ impl SequencerNodeArgs {
508533
}
509534
}
510535

536+
#[cfg(all(feature = "server", feature = "grpc"))]
537+
{
538+
self.grpc.merge(config.grpc.as_ref());
539+
}
540+
511541
self.starknet.merge(config.starknet.as_ref());
512542
self.development.merge(config.development.as_ref());
513543

@@ -881,13 +911,13 @@ explorer = true
881911
ControllerV108, ControllerV109,
882912
};
883913

884-
assert!(config.chain.genesis().classes.get(&ControllerV104::HASH).is_some());
885-
assert!(config.chain.genesis().classes.get(&ControllerV105::HASH).is_some());
886-
assert!(config.chain.genesis().classes.get(&ControllerV106::HASH).is_some());
887-
assert!(config.chain.genesis().classes.get(&ControllerV107::HASH).is_some());
888-
assert!(config.chain.genesis().classes.get(&ControllerV108::HASH).is_some());
889-
assert!(config.chain.genesis().classes.get(&ControllerV109::HASH).is_some());
890-
assert!(config.chain.genesis().classes.get(&ControllerLatest::HASH).is_some());
914+
assert!(config.chain.genesis().classes.contains_key(&ControllerV104::HASH));
915+
assert!(config.chain.genesis().classes.contains_key(&ControllerV105::HASH));
916+
assert!(config.chain.genesis().classes.contains_key(&ControllerV106::HASH));
917+
assert!(config.chain.genesis().classes.contains_key(&ControllerV107::HASH));
918+
assert!(config.chain.genesis().classes.contains_key(&ControllerV108::HASH));
919+
assert!(config.chain.genesis().classes.contains_key(&ControllerV109::HASH));
920+
assert!(config.chain.genesis().classes.contains_key(&ControllerLatest::HASH));
891921

892922
// Test without paymaster enabled
893923
let args = SequencerNodeArgs::parse_from(["katana"]);
@@ -896,12 +926,12 @@ explorer = true
896926
// Verify cartridge module is not enabled by default
897927
assert!(!config.rpc.apis.contains(&RpcModuleKind::Cartridge));
898928

899-
assert!(config.chain.genesis().classes.get(&ControllerV104::HASH).is_none());
900-
assert!(config.chain.genesis().classes.get(&ControllerV105::HASH).is_none());
901-
assert!(config.chain.genesis().classes.get(&ControllerV106::HASH).is_none());
902-
assert!(config.chain.genesis().classes.get(&ControllerV107::HASH).is_none());
903-
assert!(config.chain.genesis().classes.get(&ControllerV108::HASH).is_none());
904-
assert!(config.chain.genesis().classes.get(&ControllerV109::HASH).is_none());
905-
assert!(config.chain.genesis().classes.get(&ControllerLatest::HASH).is_none());
929+
assert!(!config.chain.genesis().classes.contains_key(&ControllerV104::HASH));
930+
assert!(!config.chain.genesis().classes.contains_key(&ControllerV105::HASH));
931+
assert!(!config.chain.genesis().classes.contains_key(&ControllerV106::HASH));
932+
assert!(!config.chain.genesis().classes.contains_key(&ControllerV107::HASH));
933+
assert!(!config.chain.genesis().classes.contains_key(&ControllerV108::HASH));
934+
assert!(!config.chain.genesis().classes.contains_key(&ControllerV109::HASH));
935+
assert!(!config.chain.genesis().classes.contains_key(&ControllerLatest::HASH));
906936
}
907937
}

crates/cli/src/file.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ pub struct NodeArgsConfig {
2626
pub server: Option<ServerOptions>,
2727
#[cfg(feature = "server")]
2828
pub metrics: Option<MetricsOptions>,
29+
#[cfg(all(feature = "server", feature = "grpc"))]
30+
pub grpc: Option<GrpcOptions>,
2931
#[cfg(feature = "cartridge")]
3032
pub cartridge: Option<CartridgeOptions>,
3133
#[cfg(feature = "explorer")]
@@ -76,6 +78,12 @@ impl TryFrom<SequencerNodeArgs> for NodeArgsConfig {
7678
if args.metrics == MetricsOptions::default() { None } else { Some(args.metrics) };
7779
}
7880

81+
#[cfg(all(feature = "server", feature = "grpc"))]
82+
{
83+
node_config.grpc =
84+
if args.grpc == GrpcOptions::default() { None } else { Some(args.grpc) };
85+
}
86+
7987
#[cfg(feature = "cartridge")]
8088
{
8189
node_config.cartridge = if args.cartridge == CartridgeOptions::default() {

crates/cli/src/options.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -835,3 +835,77 @@ impl TeeOptions {
835835
}
836836
}
837837
}
838+
839+
#[cfg(all(feature = "server", feature = "grpc"))]
840+
#[derive(Debug, Args, Clone, Serialize, Deserialize, PartialEq)]
841+
#[command(next_help_heading = "gRPC server options")]
842+
pub struct GrpcOptions {
843+
/// Enable the gRPC server.
844+
///
845+
/// When enabled, the gRPC server will start alongside the JSON-RPC server,
846+
/// providing high-performance endpoints for Starknet operations.
847+
#[arg(long = "grpc")]
848+
#[serde(default)]
849+
pub grpc_enable: bool,
850+
851+
/// gRPC server listening interface.
852+
#[arg(requires = "grpc_enable")]
853+
#[arg(long = "grpc.addr", value_name = "ADDRESS")]
854+
#[arg(default_value_t = default_grpc_addr())]
855+
#[serde(default = "default_grpc_addr")]
856+
pub grpc_addr: IpAddr,
857+
858+
/// gRPC server listening port.
859+
#[arg(requires = "grpc_enable")]
860+
#[arg(long = "grpc.port", value_name = "PORT")]
861+
#[arg(default_value_t = default_grpc_port())]
862+
#[serde(default = "default_grpc_port")]
863+
pub grpc_port: u16,
864+
865+
/// gRPC request timeout in seconds.
866+
#[arg(requires = "grpc_enable")]
867+
#[arg(long = "grpc.timeout", value_name = "TIMEOUT")]
868+
pub grpc_timeout: Option<u64>,
869+
}
870+
871+
#[cfg(all(feature = "server", feature = "grpc"))]
872+
impl Default for GrpcOptions {
873+
fn default() -> Self {
874+
GrpcOptions {
875+
grpc_enable: false,
876+
grpc_addr: default_grpc_addr(),
877+
grpc_port: default_grpc_port(),
878+
grpc_timeout: None,
879+
}
880+
}
881+
}
882+
883+
#[cfg(all(feature = "server", feature = "grpc"))]
884+
impl GrpcOptions {
885+
pub fn merge(&mut self, other: Option<&Self>) {
886+
if let Some(other) = other {
887+
if !self.grpc_enable {
888+
self.grpc_enable = other.grpc_enable;
889+
}
890+
if self.grpc_addr == default_grpc_addr() {
891+
self.grpc_addr = other.grpc_addr;
892+
}
893+
if self.grpc_port == default_grpc_port() {
894+
self.grpc_port = other.grpc_port;
895+
}
896+
if self.grpc_timeout.is_none() {
897+
self.grpc_timeout = other.grpc_timeout;
898+
}
899+
}
900+
}
901+
}
902+
903+
#[cfg(all(feature = "server", feature = "grpc"))]
904+
fn default_grpc_addr() -> IpAddr {
905+
katana_node::config::grpc::DEFAULT_GRPC_ADDR
906+
}
907+
908+
#[cfg(all(feature = "server", feature = "grpc"))]
909+
fn default_grpc_port() -> u16 {
910+
katana_node::config::grpc::DEFAULT_GRPC_PORT
911+
}

crates/grpc/Cargo.toml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,44 @@ repository.workspace = true
66
version.workspace = true
77

88
[dependencies]
9+
# Internal dependencies
10+
katana-primitives.workspace = true
11+
katana-provider.workspace = true
12+
katana-pool.workspace = true
13+
katana-rpc-types.workspace = true
14+
katana-rpc-server.workspace = true
15+
katana-rpc-api.workspace = true
16+
17+
# gRPC dependencies
918
tonic.workspace = true
19+
tonic-reflection = "0.11"
20+
prost.workspace = true
21+
http = "0.2"
22+
23+
# Async runtime
24+
tokio.workspace = true
25+
26+
# Logging
27+
tracing.workspace = true
28+
29+
# Error handling
30+
thiserror.workspace = true
31+
32+
# Serialization
33+
serde_json.workspace = true
34+
35+
# Starknet types
36+
starknet.workspace = true
37+
38+
# Cairo types
39+
cairo-lang-starknet-classes.workspace = true
40+
num-bigint.workspace = true
41+
42+
tower-service.workspace = true
1043

1144
[dev-dependencies]
45+
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
46+
hex = "0.4"
1247

1348
[build-dependencies]
1449
tonic-build.workspace = true

crates/grpc/build.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ use std::path::PathBuf;
33
fn main() -> Result<(), Box<dyn std::error::Error>> {
44
let out_dir =
55
PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR environment variable not set"));
6-
let feature_client = std::env::var("CARGO_FEATURE_CLIENT");
7-
let feature_server = std::env::var("CARGO_FEATURE_SERVER");
86

97
tonic_build::configure()
10-
.build_server(feature_server.is_ok())
11-
.build_client(feature_client.is_ok())
8+
.build_server(true)
9+
.build_client(true)
1210
.file_descriptor_set_path(out_dir.join("starknet_descriptor.bin"))
11+
// Allow clippy lints on generated code for enum variant naming and size patterns
12+
.type_attribute(".", "#[allow(clippy::enum_variant_names, clippy::large_enum_variant)]")
1313
.compile(&["proto/starknet.proto"], &["proto"])?;
1414

1515
println!("cargo:rerun-if-changed=proto");

crates/grpc/proto/common.proto

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
syntax = "proto3";
2+
3+
package common;
4+
5+
message Felt {
6+
bytes value = 1;
7+
}

0 commit comments

Comments
 (0)