diff --git a/Cargo.lock b/Cargo.lock index 37a0c252de..a935ce0edd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8419,6 +8419,9 @@ dependencies = [ name = "katana-grpc" version = "1.0.1" dependencies = [ + "katana-primitives", + "prost 0.12.6", + "thiserror", "tonic 0.11.0", "tonic-build 0.11.0", "tonic-build 0.12.3", @@ -8437,6 +8440,7 @@ dependencies = [ "katana-core", "katana-db", "katana-executor", + "katana-grpc", "katana-pipeline", "katana-pool", "katana-primitives", @@ -8447,6 +8451,9 @@ dependencies = [ "starknet 0.12.0", "strum 0.25.0", "strum_macros 0.25.3", + "tokio", + "tonic 0.11.0", + "tonic-health", "tower 0.4.13", "tower-http 0.4.4", "tracing", @@ -8566,6 +8573,7 @@ dependencies = [ "alloy-primitives", "anyhow", "assert_matches", + "async-trait", "cainome 0.4.6", "dojo-metrics", "dojo-test-utils", @@ -8576,6 +8584,7 @@ dependencies = [ "katana-cairo", "katana-core", "katana-executor", + "katana-grpc", "katana-node", "katana-pool", "katana-primitives", @@ -8595,6 +8604,7 @@ dependencies = [ "tempfile", "thiserror", "tokio", + "tonic 0.11.0", "tracing", "url", ] @@ -15255,6 +15265,19 @@ dependencies = [ "syn 2.0.77", ] +[[package]] +name = "tonic-health" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cef6e24bc96871001a7e48e820ab240b3de2201e59b517cf52835df2f1d2350" +dependencies = [ + "async-stream", + "prost 0.12.6", + "tokio", + "tokio-stream", + "tonic 0.11.0", +] + [[package]] name = "tonic-reflection" version = "0.11.0" diff --git a/Cargo.toml b/Cargo.toml index 4253650fc3..24cb39337a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,6 +97,7 @@ katana-codecs-derive = { path = "crates/katana/storage/codecs/derive" } katana-core = { path = "crates/katana/core", default-features = false } katana-db = { path = "crates/katana/storage/db" } katana-executor = { path = "crates/katana/executor" } +katana-grpc = { path = "crates/katana/grpc" } katana-node = { path = "crates/katana/node", default-features = false } katana-node-bindings = { path = "crates/katana/node-bindings" } katana-pipeline = { path = "crates/katana/pipeline" } @@ -243,6 +244,7 @@ warp = "0.3" prost = "0.12" tonic = { version = "0.11", features = [ "gzip", "tls", "tls-roots" ] } tonic-build = "0.11" +tonic-health = "0.11" tonic-reflection = "0.11" tonic-web = "0.11" diff --git a/crates/katana/grpc/Cargo.toml b/crates/katana/grpc/Cargo.toml index 97de5ad6f5..440b5f8c4c 100644 --- a/crates/katana/grpc/Cargo.toml +++ b/crates/katana/grpc/Cargo.toml @@ -6,10 +6,17 @@ repository.workspace = true version.workspace = true [dependencies] -tonic.workspace = true +katana-primitives.workspace = true -[dev-dependencies] +prost.workspace = true +thiserror.workspace = true +tonic.workspace = true [build-dependencies] tonic-build.workspace = true wasm-tonic-build.workspace = true + +[features] +client = [ ] +default = [ "server" ] +server = [ ] diff --git a/crates/katana/grpc/build.rs b/crates/katana/grpc/build.rs index 361736f8b9..411ca0e89a 100644 --- a/crates/katana/grpc/build.rs +++ b/crates/katana/grpc/build.rs @@ -7,8 +7,9 @@ fn main() -> Result<(), Box> { let feature_server = std::env::var("CARGO_FEATURE_SERVER"); tonic_build::configure() - .build_server(feature_server.is_ok()) - .build_client(feature_client.is_ok()) + // .build_server(feature_server.is_ok()) + // .build_client(feature_client.is_ok()) + .build_transport(true) .file_descriptor_set_path(out_dir.join("starknet_descriptor.bin")) .compile(&["proto/starknet.proto"], &["proto"])?; diff --git a/crates/katana/grpc/proto/starknet.proto b/crates/katana/grpc/proto/starknet.proto index 12e3265110..0472e16096 100644 --- a/crates/katana/grpc/proto/starknet.proto +++ b/crates/katana/grpc/proto/starknet.proto @@ -235,7 +235,7 @@ message BlockHashAndNumberResponse { message ChainIdRequest {} message ChainIdResponse { - string chain_id = 1; + types.Felt chain_id = 1; } message SyncingRequest {} diff --git a/crates/katana/grpc/proto/types.proto b/crates/katana/grpc/proto/types.proto index ef07757c92..cd29b9a2ed 100644 --- a/crates/katana/grpc/proto/types.proto +++ b/crates/katana/grpc/proto/types.proto @@ -10,10 +10,15 @@ message BlockID { oneof identifier { uint64 number = 1 [json_name = "block_number"]; Felt hash = 2 [json_name = "block_hash"]; - string tag = 3 [json_name = "block_tag"]; + BlockTag tag = 3 [json_name = "block_tag"]; } } +enum BlockTag { + PENDING = 0; + LATEST = 1; +} + enum SimulationFlag { SKIP_FEE_CHARGE = 0; SKIP_EXECUTE = 1; diff --git a/crates/katana/grpc/src/lib.rs b/crates/katana/grpc/src/lib.rs index d0dd5a0e60..ec5666c959 100644 --- a/crates/katana/grpc/src/lib.rs +++ b/crates/katana/grpc/src/lib.rs @@ -1 +1,77 @@ -//! gRPC implementations. +use katana_primitives::{ + block::{BlockIdOrTag, BlockTag}, + ContractAddress, Felt, +}; + +pub mod api { + tonic::include_proto!("starknet"); +} + +pub mod types { + tonic::include_proto!("types"); +} + +pub use api::starknet_server::Starknet as StarknetApi; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + Decode(#[from] ::prost::DecodeError), +} + +impl TryFrom for Felt { + type Error = Error; + + fn try_from(value: types::Felt) -> Result { + if value.value.len() > 32 { + panic!("doesn't fit") + } + + Ok(Self::from_bytes_be_slice(&value.value)) + } +} + +impl From for types::Felt { + fn from(value: Felt) -> Self { + Self { value: value.to_bytes_be().to_vec() } + } +} + +impl TryFrom for ContractAddress { + type Error = Error; + + fn try_from(value: types::Felt) -> Result { + Ok(Self::new(Felt::try_from(value)?)) + } +} + +impl From for BlockTag { + fn from(value: types::BlockTag) -> Self { + match value { + types::BlockTag::Latest => Self::Latest, + types::BlockTag::Pending => Self::Pending, + } + } +} + +impl TryFrom for BlockIdOrTag { + type Error = Error; + + fn try_from(value: types::BlockId) -> Result { + use types::block_id::Identifier; + + let Some(id) = value.identifier else { panic!("missing id") }; + + match id { + Identifier::Number(num) => Ok(Self::Number(num)), + Identifier::Hash(hash) => { + let felt = Felt::try_from(hash)?; + Ok(Self::Hash(felt)) + } + Identifier::Tag(tag) => { + let tag = types::BlockTag::try_from(tag)?; + Ok(Self::Tag(BlockTag::from(tag))) + } + } + } +} diff --git a/crates/katana/node/Cargo.toml b/crates/katana/node/Cargo.toml index fd55d0f9c7..fe91969a48 100644 --- a/crates/katana/node/Cargo.toml +++ b/crates/katana/node/Cargo.toml @@ -9,6 +9,7 @@ version.workspace = true katana-core.workspace = true katana-db.workspace = true katana-executor.workspace = true +katana-grpc.workspace = true katana-pipeline.workspace = true katana-pool.workspace = true katana-primitives.workspace = true @@ -27,9 +28,12 @@ tower = { workspace = true, features = [ "full" ] } tower-http = { workspace = true, features = [ "full" ] } tracing.workspace = true +const_format = "0.2.33" strum.workspace = true strum_macros.workspace = true -const_format = "0.2.33" +tonic.workspace = true +tonic-health.workspace = true +tokio.workspace = true [build-dependencies] vergen = { version = "9.0.0", features = [ "build", "cargo", "emit_and_set" ] } diff --git a/crates/katana/node/src/lib.rs b/crates/katana/node/src/lib.rs index e2037730a2..94d664fb1c 100644 --- a/crates/katana/node/src/lib.rs +++ b/crates/katana/node/src/lib.rs @@ -271,6 +271,9 @@ pub async fn spawn( ), config: RpcConfig, ) -> Result { + use katana_grpc::api::starknet_server::StarknetServer; + use tonic_health::server::HealthReporter; + let (pool, backend, block_producer, validator, forked_client) = node_components; let mut methods = RpcModule::new(()); @@ -292,6 +295,18 @@ pub async fn spawn( StarknetApi::new(backend.clone(), pool.clone(), block_producer.clone(), validator, cfg) }; + let (mut health_reporter, health_service) = tonic_health::server::health_reporter(); + health_reporter.set_serving::>>().await; + + tokio::spawn( + tonic::transport::Server::builder() + .add_service(health_service) + .add_service(StarknetServer::new(server.clone())) + .serve(SocketAddr::new(config.addr, 6969)), + ); + + info!("grpc server listening on port 6969"); + methods.merge(StarknetApiServer::into_rpc(server.clone()))?; methods.merge(StarknetWriteApiServer::into_rpc(server.clone()))?; methods.merge(StarknetTraceApiServer::into_rpc(server))?; diff --git a/crates/katana/rpc/rpc/Cargo.toml b/crates/katana/rpc/rpc/Cargo.toml index 6ffc5703ac..d1abb38715 100644 --- a/crates/katana/rpc/rpc/Cargo.toml +++ b/crates/katana/rpc/rpc/Cargo.toml @@ -8,11 +8,13 @@ version.workspace = true [dependencies] anyhow.workspace = true +async-trait.workspace = true dojo-metrics.workspace = true futures.workspace = true jsonrpsee = { workspace = true, features = [ "server" ] } katana-core.workspace = true katana-executor.workspace = true +katana-grpc.workspace = true katana-pool.workspace = true katana-primitives.workspace = true katana-provider.workspace = true @@ -24,6 +26,7 @@ metrics.workspace = true starknet.workspace = true thiserror.workspace = true tokio.workspace = true +tonic.workspace = true tracing.workspace = true url.workspace = true diff --git a/crates/katana/rpc/rpc/src/starknet/grpc.rs b/crates/katana/rpc/rpc/src/starknet/grpc.rs new file mode 100644 index 0000000000..7587ff5af0 --- /dev/null +++ b/crates/katana/rpc/rpc/src/starknet/grpc.rs @@ -0,0 +1,224 @@ +use katana_executor::ExecutorFactory; +use katana_grpc::api::{ + BlockHashAndNumberRequest, BlockHashAndNumberResponse, BlockNumberRequest, BlockNumberResponse, + CallRequest, CallResponse, ChainIdRequest, ChainIdResponse, EstimateFeeRequest, + EstimateFeeResponse, EstimateMessageFeeRequest, GetBlockRequest, + GetBlockTransactionCountResponse, GetBlockWithReceiptsResponse, GetBlockWithTxHashesResponse, + GetBlockWithTxsResponse, GetClassAtRequest, GetClassAtResponse, GetClassHashAtRequest, + GetClassHashAtResponse, GetClassRequest, GetClassResponse, GetEventsRequest, GetEventsResponse, + GetNonceRequest, GetNonceResponse, GetStateUpdateResponse, GetStorageAtRequest, + GetStorageAtResponse, GetTransactionByBlockIdAndIndexRequest, + GetTransactionByBlockIdAndIndexResponse, GetTransactionByHashRequest, + GetTransactionByHashResponse, GetTransactionReceiptRequest, GetTransactionReceiptResponse, + GetTransactionStatusRequest, GetTransactionStatusResponse, SpecVersionRequest, + SpecVersionResponse, SyncingRequest, SyncingResponse, +}; +use katana_primitives::block::BlockIdOrTag; +use katana_primitives::contract::StorageKey; +use katana_primitives::ContractAddress; +use tonic::{Request, Response, Status}; + +const RPC_SPEC_VERSION: &str = "0.7.1"; + +#[tonic::async_trait] +impl katana_grpc::StarknetApi for super::StarknetApi { + async fn spec_version( + &self, + _: Request, + ) -> Result, Status> { + let message = SpecVersionResponse { version: RPC_SPEC_VERSION.to_string() }; + Ok(Response::new(message)) + } + + async fn get_block_with_tx_hashes( + &self, + _request: Request, + ) -> Result, Status> { + todo!() + } + + async fn get_block_with_txs( + &self, + _request: Request, + ) -> Result, Status> { + todo!() + } + + async fn get_block_with_receipts( + &self, + _request: Request, + ) -> Result, Status> { + todo!() + } + + async fn get_state_update( + &self, + _request: Request, + ) -> Result, Status> { + todo!() + } + + async fn get_storage_at( + &self, + request: Request, + ) -> Result, Status> { + let GetStorageAtRequest { block_id, contract_address, key } = request.into_inner(); + + let block_id: BlockIdOrTag = block_id.unwrap().try_into().unwrap(); + let address: ContractAddress = contract_address.unwrap().try_into().unwrap(); + let key: StorageKey = key.unwrap().try_into().unwrap(); + + let value = self.storage_at(address, key, block_id).unwrap(); + let value = katana_grpc::types::Felt::from(value); + + let message = GetStorageAtResponse { value: Some(value) }; + Ok(Response::new(message)) + } + + async fn get_transaction_status( + &self, + _request: Request, + ) -> Result, Status> { + todo!() + } + + async fn get_transaction_by_hash( + &self, + _request: Request, + ) -> Result, Status> { + todo!() + } + + async fn get_transaction_by_block_id_and_index( + &self, + _request: Request, + ) -> Result, Status> { + todo!() + } + + async fn get_transaction_receipt( + &self, + _request: Request, + ) -> Result, Status> { + todo!() + } + + async fn get_class( + &self, + _request: Request, + ) -> Result, Status> { + todo!() + } + + async fn get_class_hash_at( + &self, + request: Request, + ) -> Result, Status> { + let GetClassHashAtRequest { block_id, contract_address } = request.into_inner(); + + let block_id: BlockIdOrTag = block_id.unwrap().try_into().unwrap(); + let address: ContractAddress = contract_address.unwrap().try_into().unwrap(); + + let class_hash = self.class_hash_at_address(block_id, address).await.unwrap(); + let class_hash = katana_grpc::types::Felt::from(class_hash); + + let message = GetClassHashAtResponse { class_hash: Some(class_hash) }; + Ok(Response::new(message)) + } + + async fn get_class_at( + &self, + _request: Request, + ) -> Result, Status> { + todo!() + } + + async fn get_block_transaction_count( + &self, + request: Request, + ) -> Result, Status> { + let GetBlockRequest { block_id } = request.into_inner(); + let block_id: BlockIdOrTag = block_id.unwrap().try_into().unwrap(); + + let count = self.block_tx_count(block_id).await.unwrap(); + let message = GetBlockTransactionCountResponse { count }; + + Ok(Response::new(message)) + } + + async fn call(&self, _request: Request) -> Result, Status> { + todo!() + } + + async fn estimate_fee( + &self, + _request: Request, + ) -> Result, Status> { + todo!() + } + + async fn estimate_message_fee( + &self, + _request: Request, + ) -> Result, Status> { + todo!() + } + + async fn block_number( + &self, + _: Request, + ) -> Result, Status> { + let block_number = self.latest_block_number().await.unwrap(); + let message = BlockNumberResponse { block_number }; + Ok(Response::new(message)) + } + + async fn block_hash_and_number( + &self, + _: Request, + ) -> Result, Status> { + let (block_hash, block_number) = self.block_hash_and_number().unwrap(); + let message = + BlockHashAndNumberResponse { block_number, block_hash: Some(block_hash.into()) }; + Ok(Response::new(message)) + } + + async fn chain_id( + &self, + _: Request, + ) -> Result, Status> { + let id = self.inner.backend.chain_spec.id.id(); + let id = katana_grpc::types::Felt::from(id); + let message = ChainIdResponse { chain_id: Some(id) }; + Ok(Response::new(message)) + } + + async fn syncing( + &self, + _request: Request, + ) -> Result, Status> { + todo!() + } + + async fn get_events( + &self, + _request: Request, + ) -> Result, Status> { + todo!() + } + + async fn get_nonce( + &self, + request: Request, + ) -> Result, Status> { + let GetNonceRequest { block_id, contract_address } = request.into_inner(); + let block_id: BlockIdOrTag = block_id.unwrap().try_into().unwrap(); + let address: ContractAddress = contract_address.unwrap().try_into().unwrap(); + + let nonce = self.nonce_at(block_id, address).await.unwrap(); + let nonce = katana_grpc::types::Felt::from(nonce); + + let message = GetNonceResponse { nonce: Some(nonce) }; + Ok(Response::new(message)) + } +} diff --git a/crates/katana/rpc/rpc/src/starknet/mod.rs b/crates/katana/rpc/rpc/src/starknet/mod.rs index c17cb790c3..db74385fda 100644 --- a/crates/katana/rpc/rpc/src/starknet/mod.rs +++ b/crates/katana/rpc/rpc/src/starknet/mod.rs @@ -1,6 +1,7 @@ //! Server implementation for the Starknet JSON-RPC API. pub mod forking; +mod grpc; mod read; mod trace; mod write; diff --git a/examples/spawn-and-move/Scarb.lock b/examples/spawn-and-move/Scarb.lock index a3ce4d9320..95abffec9e 100644 --- a/examples/spawn-and-move/Scarb.lock +++ b/examples/spawn-and-move/Scarb.lock @@ -31,7 +31,7 @@ dependencies = [ [[package]] name = "dojo_examples" -version = "1.0.0" +version = "1.0.1" dependencies = [ "armory", "bestiary",