From f20477800b0c9c53d8ceb02d9cc735d493c26581 Mon Sep 17 00:00:00 2001 From: Mirko von Leipzig <48352201+Mirko-von-Leipzig@users.noreply.github.com> Date: Fri, 27 Feb 2026 09:31:17 +0200 Subject: [PATCH 01/24] Use codegen for proto code generation --- Cargo.lock | 10 ++ Cargo.toml | 1 + crates/proto/build.rs | 8 +- crates/store/src/server/mod.rs | 10 +- proto/Cargo.toml | 5 + proto/build.rs | 170 ++++++++++++++++++--------------- proto/src/lib.rs | 66 +------------ 7 files changed, 115 insertions(+), 155 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ffe72aec61..e527210a9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1063,6 +1063,15 @@ dependencies = [ "cc", ] +[[package]] +name = "codegen" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "573800db6c3319bc125ddbf9b9cb001ad1602957f53642ba8d09ff3ddd4da7f1" +dependencies = [ + "indexmap", +] + [[package]] name = "colorchoice" version = "1.0.5" @@ -3303,6 +3312,7 @@ name = "miden-node-proto-build" version = "0.15.0" dependencies = [ "build-rs", + "codegen", "fs-err", "miette", "protox", diff --git a/Cargo.toml b/Cargo.toml index 7bc7d1ede9..c13e492a89 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,6 +78,7 @@ assert_matches = { version = "1.5" } async-trait = { version = "0.1" } build-rs = { version = "0.3" } clap = { features = ["derive"], version = "4.5" } +codegen = { version = "0.3" } deadpool = { default-features = false, version = "0.12" } deadpool-diesel = { version = "0.6" } deadpool-sync = { default-features = false, version = "0.1" } diff --git a/crates/proto/build.rs b/crates/proto/build.rs index 4c3d38ab47..4bde2f2f73 100644 --- a/crates/proto/build.rs +++ b/crates/proto/build.rs @@ -6,9 +6,7 @@ use miden_node_proto_build::{ ntx_builder_api_descriptor, remote_prover_api_descriptor, rpc_api_descriptor, - store_block_producer_api_descriptor, - store_ntx_builder_api_descriptor, - store_rpc_api_descriptor, + store_api_descriptor, validator_api_descriptor, }; use miette::{Context, IntoDiagnostic}; @@ -25,9 +23,7 @@ fn main() -> miette::Result<()> { .wrap_err("creating destination folder")?; generate_bindings(rpc_api_descriptor(), &dst_dir)?; - generate_bindings(store_rpc_api_descriptor(), &dst_dir)?; - generate_bindings(store_ntx_builder_api_descriptor(), &dst_dir)?; - generate_bindings(store_block_producer_api_descriptor(), &dst_dir)?; + generate_bindings(store_api_descriptor(), &dst_dir)?; generate_bindings(block_producer_api_descriptor(), &dst_dir)?; generate_bindings(remote_prover_api_descriptor(), &dst_dir)?; generate_bindings(validator_api_descriptor(), &dst_dir)?; diff --git a/crates/store/src/server/mod.rs b/crates/store/src/server/mod.rs index 34f983775d..bcd8689bea 100644 --- a/crates/store/src/server/mod.rs +++ b/crates/store/src/server/mod.rs @@ -6,11 +6,7 @@ use std::time::Duration; use anyhow::Context; use miden_node_proto::generated::store; -use miden_node_proto_build::{ - store_block_producer_api_descriptor, - store_ntx_builder_api_descriptor, - store_rpc_api_descriptor, -}; +use miden_node_proto_build::store_api_descriptor; use miden_node_utils::clap::{GrpcOptionsInternal, StorageOptions}; use miden_node_utils::panic::{CatchPanicLayer, catch_panic_layer_fn}; use miden_node_utils::tracing::grpc::grpc_trace_fn; @@ -198,9 +194,7 @@ impl Store { chain_tip_sender, }); let reflection_service = tonic_reflection::server::Builder::configure() - .register_file_descriptor_set(store_rpc_api_descriptor()) - .register_file_descriptor_set(store_ntx_builder_api_descriptor()) - .register_file_descriptor_set(store_block_producer_api_descriptor()) + .register_file_descriptor_set(store_api_descriptor()) .build_v1() .context("failed to build reflection service")?; diff --git a/proto/Cargo.toml b/proto/Cargo.toml index ee79d7adc1..95ddd8ef88 100644 --- a/proto/Cargo.toml +++ b/proto/Cargo.toml @@ -25,6 +25,11 @@ tonic-prost-build = { workspace = true } [build-dependencies] build-rs = { workspace = true } +codegen = { workspace = true } fs-err = { workspace = true } miette = { version = "7.6" } protox = { workspace = true } + +[package.metadata.cargo-machete] +# Machete misses these because they're required in files generated by build.rs. +ignored = ["protox", "tonic-prost-build"] diff --git a/proto/build.rs b/proto/build.rs index c4c2f9b924..cb5c038e3b 100644 --- a/proto/build.rs +++ b/proto/build.rs @@ -1,87 +1,105 @@ +use std::ffi::OsStr; +use std::path::PathBuf; + use fs_err as fs; -use miette::{Context, IntoDiagnostic}; +use miette::{IntoDiagnostic, miette}; use protox::prost::Message; -const RPC_PROTO: &str = "rpc.proto"; -// Unified internal store API (store.Rpc, store.BlockProducer, store.NtxBuilder). -// We compile the same file three times to preserve existing descriptor names. -const STORE_RPC_PROTO: &str = "internal/store.proto"; -const STORE_NTX_BUILDER_PROTO: &str = "internal/store.proto"; -const STORE_BLOCK_PRODUCER_PROTO: &str = "internal/store.proto"; -const BLOCK_PRODUCER_PROTO: &str = "internal/block_producer.proto"; -const REMOTE_PROVER_PROTO: &str = "remote_prover.proto"; -const VALIDATOR_PROTO: &str = "internal/validator.proto"; -const NTX_BUILDER_PROTO: &str = "internal/ntx_builder.proto"; - -const RPC_DESCRIPTOR: &str = "rpc_file_descriptor.bin"; -const STORE_RPC_DESCRIPTOR: &str = "store_rpc_file_descriptor.bin"; -const STORE_NTX_BUILDER_DESCRIPTOR: &str = "store_ntx_builder_file_descriptor.bin"; -const STORE_BLOCK_PRODUCER_DESCRIPTOR: &str = "store_block_producer_file_descriptor.bin"; -const BLOCK_PRODUCER_DESCRIPTOR: &str = "block_producer_file_descriptor.bin"; -const REMOTE_PROVER_DESCRIPTOR: &str = "remote_prover_file_descriptor.bin"; -const VALIDATOR_DESCRIPTOR: &str = "validator_file_descriptor.bin"; -const NTX_BUILDER_DESCRIPTOR: &str = "ntx_builder_file_descriptor.bin"; - -/// Generates Rust protobuf bindings from .proto files. +/// Compiles each gRPC service definitions into a +/// [`FileDescriptorSet`](tonic_prost_build::FileDescriptorSet) and exposes it as a function: /// -/// This is done only if `BUILD_PROTO` environment variable is set to `1` to avoid running the -/// script on crates.io where repo-level .proto files are not available. +/// ```rust +/// fn _api_descriptor() -> FileDescriptorSet; +/// ``` fn main() -> miette::Result<()> { build_rs::output::rerun_if_changed("./proto"); + build_rs::output::rerun_if_changed("Cargo.toml"); let out_dir = build_rs::input::out_dir(); - let crate_root = build_rs::input::cargo_manifest_dir(); - let proto_src_dir = crate_root.join("proto"); - let includes = &[proto_src_dir]; - - let rpc_file_descriptor = protox::compile([RPC_PROTO], includes)?; - let rpc_path = out_dir.join(RPC_DESCRIPTOR); - fs::write(&rpc_path, rpc_file_descriptor.encode_to_vec()) - .into_diagnostic() - .wrap_err("writing rpc file descriptor")?; - - let remote_prover_file_descriptor = protox::compile([REMOTE_PROVER_PROTO], includes)?; - let remote_prover_path = out_dir.join(REMOTE_PROVER_DESCRIPTOR); - fs::write(&remote_prover_path, remote_prover_file_descriptor.encode_to_vec()) - .into_diagnostic() - .wrap_err("writing remote prover file descriptor")?; - - let store_rpc_file_descriptor = protox::compile([STORE_RPC_PROTO], includes)?; - let store_rpc_path = out_dir.join(STORE_RPC_DESCRIPTOR); - fs::write(&store_rpc_path, store_rpc_file_descriptor.encode_to_vec()) - .into_diagnostic() - .wrap_err("writing store rpc file descriptor")?; - - let store_ntx_builder_file_descriptor = protox::compile([STORE_NTX_BUILDER_PROTO], includes)?; - let store_ntx_builder_path = out_dir.join(STORE_NTX_BUILDER_DESCRIPTOR); - fs::write(&store_ntx_builder_path, store_ntx_builder_file_descriptor.encode_to_vec()) - .into_diagnostic() - .wrap_err("writing store ntx builder file descriptor")?; - - let store_block_producer_file_descriptor = - protox::compile([STORE_BLOCK_PRODUCER_PROTO], includes)?; - let store_block_producer_path = out_dir.join(STORE_BLOCK_PRODUCER_DESCRIPTOR); - fs::write(&store_block_producer_path, store_block_producer_file_descriptor.encode_to_vec()) - .into_diagnostic() - .wrap_err("writing store block producer file descriptor")?; - - let block_producer_file_descriptor = protox::compile([BLOCK_PRODUCER_PROTO], includes)?; - let block_producer_path = out_dir.join(BLOCK_PRODUCER_DESCRIPTOR); - fs::write(&block_producer_path, block_producer_file_descriptor.encode_to_vec()) - .into_diagnostic() - .wrap_err("writing block producer file descriptor")?; - - let validator_file_descriptor = protox::compile([VALIDATOR_PROTO], includes)?; - let validator_path = out_dir.join(VALIDATOR_DESCRIPTOR); - fs::write(&validator_path, validator_file_descriptor.encode_to_vec()) - .into_diagnostic() - .wrap_err("writing validator file descriptor")?; - - let ntx_builder_file_descriptor = protox::compile([NTX_BUILDER_PROTO], includes)?; - let ntx_builder_path = out_dir.join(NTX_BUILDER_DESCRIPTOR); - fs::write(&ntx_builder_path, ntx_builder_file_descriptor.encode_to_vec()) - .into_diagnostic() - .wrap_err("writing ntx builder file descriptor")?; + let schema_dir = build_rs::input::cargo_manifest_dir().join("proto"); + + // Codegen which will hold the file descriptor functions. + // + // `protox::prost::Message` is a trait which brings into scope the encoding and decoding of file + // descriptors. This is required so because we serialize the descriptors in code as a `Vec` + // and then decode it again inline. + let mut code = codegen::Scope::new(); + code.import("tonic_prost_build", "FileDescriptorSet"); + code.import("protox::prost", "Message"); + + // We split our gRPC services into public and internal. + // + // This is easy to do since public services are listed in the root of the schema folder, + // and internal services are nested in the `internal` folder. + for public_api in proto_files_in_directory(&schema_dir)? { + let file_descriptor_fn = generate_file_descriptor(&public_api, &schema_dir)?; + code.push_fn(file_descriptor_fn); + } + + // Internal gRPC services need an additional feature gate `#[cfg(feature = "internal")]`. + for internal_api in proto_files_in_directory(&schema_dir.join("internal"))? { + let mut file_descriptor_fn = generate_file_descriptor(&internal_api, &schema_dir)?; + file_descriptor_fn.attr("cfg(feature = \"internal\")"); + code.push_fn(file_descriptor_fn); + } + + fs::write(out_dir.join("file_descriptors.rs"), code.to_string()).into_diagnostic()?; Ok(()) } + +/// The list of `*.proto` files in the given directory. +/// +/// Does _not_ recurse into folders; only top level files are returned. +fn proto_files_in_directory(directory: &PathBuf) -> Result, miette::Error> { + let mut proto_files = Vec::new(); + for entry in fs::read_dir(directory).into_diagnostic()? { + let entry = entry.into_diagnostic()?; + + // Skip non-files + if !entry.file_type().into_diagnostic()?.is_file() { + continue; + } + + // Skip non-protobuf files + if PathBuf::from(entry.file_name()).extension().is_none_or(|ext| ext != "proto") { + continue; + } + + proto_files.push(entry.path()); + } + Ok(proto_files) +} + +/// Creates a function which emits the file descriptor of the given gRPC service file. +/// +/// The function looks as follows: +/// +/// ```rust +/// fn _api_descriptor() -> FileDescriptorSet { +/// FileDescriptorSet::decode(vec![].as_slice()) +/// .expect("encoded file descriptor should decode") +/// } +/// ``` +/// +/// where `` is bytes of the compiled gRPC service. +fn generate_file_descriptor( + grpc_service: &PathBuf, + includes: &PathBuf, +) -> Result { + let file_name = grpc_service + .file_stem() + .and_then(OsStr::to_str) + .ok_or_else(|| miette!("invalid file name for {grpc_service:?}"))?; + + let file_descriptor = protox::compile([grpc_service], includes)?; + let file_descriptor = file_descriptor.encode_to_vec(); + + let mut f = codegen::Function::new(format!("{file_name}_api_descriptor")); + f.vis("pub") + .ret("FileDescriptorSet") + .line(format!("FileDescriptorSet::decode(vec!{file_descriptor:?}.as_slice())")) + .line(".expect(\"we just encoded this so it should decode\")"); + + Ok(f) +} diff --git a/proto/src/lib.rs b/proto/src/lib.rs index 6cbc6eb015..f38cc3ad50 100644 --- a/proto/src/lib.rs +++ b/proto/src/lib.rs @@ -1,65 +1 @@ -use protox::prost::Message; -use tonic_prost_build::FileDescriptorSet; - -/// Returns the Protobuf file descriptor for the RPC API. -pub fn rpc_api_descriptor() -> FileDescriptorSet { - let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/", "rpc_file_descriptor.bin")); - FileDescriptorSet::decode(&bytes[..]) - .expect("bytes should be a valid file descriptor created by build.rs") -} - -/// Returns the Protobuf file descriptor for the remote prover API. -pub fn remote_prover_api_descriptor() -> FileDescriptorSet { - let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/", "remote_prover_file_descriptor.bin")); - FileDescriptorSet::decode(&bytes[..]) - .expect("bytes should be a valid file descriptor created by build.rs") -} - -/// Returns the Protobuf file descriptor for the store RPC API. -#[cfg(feature = "internal")] -pub fn store_rpc_api_descriptor() -> FileDescriptorSet { - let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/", "store_rpc_file_descriptor.bin")); - FileDescriptorSet::decode(&bytes[..]) - .expect("bytes should be a valid file descriptor created by build.rs") -} - -/// Returns the Protobuf file descriptor for the store NTX builder API. -#[cfg(feature = "internal")] -pub fn store_ntx_builder_api_descriptor() -> FileDescriptorSet { - let bytes = - include_bytes!(concat!(env!("OUT_DIR"), "/", "store_ntx_builder_file_descriptor.bin")); - FileDescriptorSet::decode(&bytes[..]) - .expect("bytes should be a valid file descriptor created by build.rs") -} - -/// Returns the Protobuf file descriptor for the store block producer API. -#[cfg(feature = "internal")] -pub fn store_block_producer_api_descriptor() -> FileDescriptorSet { - let bytes = - include_bytes!(concat!(env!("OUT_DIR"), "/", "store_block_producer_file_descriptor.bin")); - FileDescriptorSet::decode(&bytes[..]) - .expect("bytes should be a valid file descriptor created by build.rs") -} - -/// Returns the Protobuf file descriptor for the block-producer API. -#[cfg(feature = "internal")] -pub fn block_producer_api_descriptor() -> FileDescriptorSet { - let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/", "block_producer_file_descriptor.bin")); - FileDescriptorSet::decode(&bytes[..]) - .expect("bytes should be a valid file descriptor created by build.rs") -} - -/// Returns the Protobuf file descriptor for the validator API. -pub fn validator_api_descriptor() -> FileDescriptorSet { - let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/", "validator_file_descriptor.bin")); - FileDescriptorSet::decode(&bytes[..]) - .expect("bytes should be a valid file descriptor created by build.rs") -} - -/// Returns the Protobuf file descriptor for the NTX builder API. -#[cfg(feature = "internal")] -pub fn ntx_builder_api_descriptor() -> FileDescriptorSet { - let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/", "ntx_builder_file_descriptor.bin")); - FileDescriptorSet::decode(&bytes[..]) - .expect("bytes should be a valid file descriptor created by build.rs") -} +include!(concat!(env!("OUT_DIR"), "/file_descriptors.rs")); From af136ddf67ed851bda950a63e8f9d8294351637a Mon Sep 17 00:00:00 2001 From: Krisztian Kovacs Date: Thu, 16 Apr 2026 09:04:54 +0200 Subject: [PATCH 02/24] fix: use Path references instead of PathBuf --- proto/build.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/proto/build.rs b/proto/build.rs index cb5c038e3b..3f061d426d 100644 --- a/proto/build.rs +++ b/proto/build.rs @@ -1,5 +1,5 @@ use std::ffi::OsStr; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use fs_err as fs; use miette::{IntoDiagnostic, miette}; @@ -51,7 +51,7 @@ fn main() -> miette::Result<()> { /// The list of `*.proto` files in the given directory. /// /// Does _not_ recurse into folders; only top level files are returned. -fn proto_files_in_directory(directory: &PathBuf) -> Result, miette::Error> { +fn proto_files_in_directory(directory: &Path) -> Result, miette::Error> { let mut proto_files = Vec::new(); for entry in fs::read_dir(directory).into_diagnostic()? { let entry = entry.into_diagnostic()?; @@ -84,8 +84,8 @@ fn proto_files_in_directory(directory: &PathBuf) -> Result, miette: /// /// where `` is bytes of the compiled gRPC service. fn generate_file_descriptor( - grpc_service: &PathBuf, - includes: &PathBuf, + grpc_service: &Path, + includes: &Path, ) -> Result { let file_name = grpc_service .file_stem() From e8b9f06fffc26c88d42a60416596ff60fec5732e Mon Sep 17 00:00:00 2001 From: Mirko von Leipzig <48352201+Mirko-von-Leipzig@users.noreply.github.com> Date: Thu, 26 Feb 2026 14:32:59 +0200 Subject: [PATCH 03/24] gRPC server traits and codec --- Cargo.lock | 1 + crates/proto/Cargo.toml | 1 + crates/proto/src/lib.rs | 1 + crates/proto/src/server/mod.rs | 69 ++++++++++++++++++++++ crates/proto/src/server/remote_prover.rs | 74 ++++++++++++++++++++++++ 5 files changed, 146 insertions(+) create mode 100644 crates/proto/src/server/mod.rs create mode 100644 crates/proto/src/server/remote_prover.rs diff --git a/Cargo.lock b/Cargo.lock index e527210a9f..83c5a4648f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3290,6 +3290,7 @@ dependencies = [ "assert_matches", "build-rs", "fs-err", + "futures", "hex", "http 1.4.0", "miden-node-grpc-error-macro", diff --git a/crates/proto/Cargo.toml b/crates/proto/Cargo.toml index fa48024ce5..35fbb81217 100644 --- a/crates/proto/Cargo.toml +++ b/crates/proto/Cargo.toml @@ -16,6 +16,7 @@ workspace = true [dependencies] anyhow = { workspace = true } +futures = { workspace = true } hex = { version = "0.4" } http = { workspace = true } miden-node-grpc-error-macro = { workspace = true } diff --git a/crates/proto/src/lib.rs b/crates/proto/src/lib.rs index 1ec05672cf..29e9e77b51 100644 --- a/crates/proto/src/lib.rs +++ b/crates/proto/src/lib.rs @@ -2,6 +2,7 @@ pub mod clients; pub mod decode; pub mod domain; pub mod errors; +pub mod server; #[rustfmt::skip] pub mod generated; diff --git a/crates/proto/src/server/mod.rs b/crates/proto/src/server/mod.rs new file mode 100644 index 0000000000..ce26c3c6ef --- /dev/null +++ b/crates/proto/src/server/mod.rs @@ -0,0 +1,69 @@ +pub mod remote_prover; + +use core::fmt::Display; + +use futures::stream::Stream; +use tonic::{Request, Response, Status}; + +/// Decode a gRPC request body into a domain input type. +pub trait GrpcDecode: Sized + Send + Sync + 'static { + type Error: Display + Send + Sync + 'static; + + fn decode(input: T) -> Result; +} + +/// Encode a domain output into a gRPC response body. +/// +/// The encode consumes `self` so implementors can move out of the output. +pub trait GrpcEncode: Send + Sync + 'static { + fn encode(self) -> Result; +} + +pub trait GrpcInterface { + type Request; + type Response; +} + +/// Unary method handler. +/// +/// The method marker `M` is used to disambiguate multiple methods that share request/response +/// types. +#[tonic::async_trait] +pub trait GrpcUnary: Send + Sync + 'static { + type Input: GrpcDecode; + type Output: GrpcEncode; + + async fn handle(&self, input: Self::Input) -> Result; +} + +/// Server-streaming method handler. +/// +/// The method marker `M` is used to disambiguate multiple methods that share request/response +/// types. +#[tonic::async_trait] +pub trait GrpcServerStream: Send + Sync + 'static { + type Input: GrpcDecode; + type Stream: Stream> + Send + 'static; + + async fn handle(&self, input: Self::Input) -> Result; +} + +/// Execute the standard unary flow: decode → handle → encode. +/// +/// Decode errors are mapped to `Status::invalid_argument`. +pub async fn handle_unary( + handler: &Handler, + request: Request, +) -> Result, Status> +where + Handler: GrpcUnary, + Handler::Input: GrpcDecode, + Handler::Output: GrpcEncode, + Method: GrpcInterface, +{ + let input = Handler::Input::decode(request.into_inner()) + .map_err(|err| Status::invalid_argument(err.to_string()))?; + let output = handler.handle(input).await?; + let response = output.encode()?; + Ok(Response::new(response)) +} diff --git a/crates/proto/src/server/remote_prover.rs b/crates/proto/src/server/remote_prover.rs new file mode 100644 index 0000000000..c2e6b7c445 --- /dev/null +++ b/crates/proto/src/server/remote_prover.rs @@ -0,0 +1,74 @@ +use std::task::{Context, Poll}; + +use tonic::{Request, Response, Status}; + +use crate::generated::remote_prover::{self as proto, api_server}; +use crate::server::{GrpcInterface, GrpcUnary, handle_unary}; + +pub struct ProveMethod; + +impl GrpcInterface for ProveMethod { + type Request = proto::ProofRequest; + type Response = proto::Proof; +} + +/// Public server trait for the remote prover API. +pub trait RemoteProverService: GrpcUnary {} + +impl RemoteProverService for T where T: GrpcUnary {} + +#[tonic::async_trait] +impl api_server::Api for T +where + T: RemoteProverService, +{ + async fn prove( + &self, + request: Request, + ) -> Result, Status> { + handle_unary::<_, ProveMethod>(self, request).await + } +} + +pub struct RemoteProverServer { + inner: api_server::ApiServer, +} + +impl RemoteProverServer +where + T: RemoteProverService, +{ + pub fn new(service: T) -> Self { + Self { + inner: api_server::ApiServer::new(service), + } + } +} + +impl Clone for RemoteProverServer { + fn clone(&self) -> Self { + Self { inner: self.inner.clone() } + } +} + +impl tonic::codegen::Service> for RemoteProverServer +where + api_server::ApiServer: tonic::codegen::Service>, +{ + type Response = + as tonic::codegen::Service>>::Response; + type Error = as tonic::codegen::Service>>::Error; + type Future = as tonic::codegen::Service>>::Future; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, req: http::Request) -> Self::Future { + self.inner.call(req) + } +} + +impl tonic::server::NamedService for RemoteProverServer { + const NAME: &'static str = api_server::SERVICE_NAME; +} From 6f8fcf430e47284456eeb9e2236d03c18cb64c8a Mon Sep 17 00:00:00 2001 From: Mirko von Leipzig <48352201+Mirko-von-Leipzig@users.noreply.github.com> Date: Thu, 26 Feb 2026 18:41:10 +0200 Subject: [PATCH 04/24] WIP codegen --- Cargo.lock | 1 + Cargo.toml | 1 + crates/proto/Cargo.toml | 1 + crates/proto/build.rs | 379 +++++++++++++++++++++++++++++++++++++++- 4 files changed, 375 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 83c5a4648f..0cea02269f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3301,6 +3301,7 @@ dependencies = [ "miette", "proptest", "prost", + "prost-types", "thiserror 2.0.18", "tonic", "tonic-prost", diff --git a/Cargo.toml b/Cargo.toml index c13e492a89..192acc98a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -98,6 +98,7 @@ pretty_assertions = { version = "1.4" } # lockstep, nor are they adhering to semver semantics. We keep this # to avoid future breakage. prost = { default-features = false, version = "=0.14.3" } +prost-types = { default-features = false, version = "=0.14.3" } protox = { version = "=0.9.1" } rand = { version = "0.9" } rand_chacha = { default-features = false, version = "0.9" } diff --git a/crates/proto/Cargo.toml b/crates/proto/Cargo.toml index 35fbb81217..1c3133013f 100644 --- a/crates/proto/Cargo.toml +++ b/crates/proto/Cargo.toml @@ -38,6 +38,7 @@ build-rs = { workspace = true } fs-err = { workspace = true } miden-node-proto-build = { features = ["internal"], workspace = true } miette = { version = "7.6" } +prost-types = { workspace = true } tonic-prost-build = { workspace = true } [package.metadata.cargo-machete] diff --git a/crates/proto/build.rs b/crates/proto/build.rs index 4bde2f2f73..021f3bb9e9 100644 --- a/crates/proto/build.rs +++ b/crates/proto/build.rs @@ -1,3 +1,4 @@ +use std::fmt::Write; use std::path::Path; use fs_err as fs; @@ -22,14 +23,28 @@ fn main() -> miette::Result<()> { .into_diagnostic() .wrap_err("creating destination folder")?; - generate_bindings(rpc_api_descriptor(), &dst_dir)?; - generate_bindings(store_api_descriptor(), &dst_dir)?; - generate_bindings(block_producer_api_descriptor(), &dst_dir)?; - generate_bindings(remote_prover_api_descriptor(), &dst_dir)?; - generate_bindings(validator_api_descriptor(), &dst_dir)?; - generate_bindings(ntx_builder_api_descriptor(), &dst_dir)?; + let descriptor_sets = [ + rpc_api_descriptor(), + store_api_descriptor(), + block_producer_api_descriptor(), + remote_prover_api_descriptor(), + validator_api_descriptor(), + ntx_builder_api_descriptor(), + ]; - generate_mod_rs(&dst_dir).into_diagnostic().wrap_err("generating mod.rs")?; + for file_descriptors in &descriptor_sets { + generate_bindings(file_descriptors.clone(), &dst_dir)?; + } + + let server_dst_dir = dst_dir.join("server"); + fs::create_dir_all(&server_dst_dir) + .into_diagnostic() + .wrap_err("creating server destination folder")?; + + generate_server_modules(&descriptor_sets, &server_dst_dir)?; + generate_mod_rs(dst_dir) + .into_diagnostic() + .wrap_err("generating server mod.rs")?; Ok(()) } @@ -67,6 +82,14 @@ fn generate_mod_rs(dst_dir: impl AsRef) -> std::io::Result<()> { .to_owned(); submodules.push(file_stem); + } else if path.is_dir() { + let dir_name = path + .file_name() + .and_then(|f| f.to_str()) + .expect("Could not get directory name") + .to_owned(); + + submodules.push(dir_name); } } @@ -77,3 +100,345 @@ fn generate_mod_rs(dst_dir: impl AsRef) -> std::io::Result<()> { fs::write(mod_filepath, contents) } + +/// Generate server facade modules (one per service) from the provided descriptor sets. +fn generate_server_modules( + descriptor_sets: &[FileDescriptorSet], + dst_dir: &Path, +) -> miette::Result<()> { + for fds in descriptor_sets { + for file in &fds.file { + let package = file.package.as_deref().unwrap_or_default(); + let package_module = package.replace('.', "::"); + let package_prefix = package.replace('.', "_"); + + for service in &file.service { + let service_name = service.name.as_deref().unwrap_or("Service"); + let module_name = service_module_name(&package_prefix, service_name); + let server_module = format!("{}_server", to_snake_case(service_name)); + + let contents = + render_service_module(&package_module, service_name, &server_module, service); + + let path = dst_dir.join(format!("{module_name}.rs")); + fs::write(path, contents).into_diagnostic().wrap_err("writing server module")?; + } + } + } + + Ok(()) +} + +#[expect(clippy::too_many_lines, reason = "Will split later")] +fn render_service_module( + package_module: &str, + service_name: &str, + server_module: &str, + service: &prost_types::ServiceDescriptorProto, +) -> String { + let mut out = String::new(); + + writeln!(out, "use std::task::{{Context, Poll}};").unwrap(); + writeln!(out).unwrap(); + writeln!(out, "use tonic::{{Request, Response, Status}};").unwrap(); + writeln!(out).unwrap(); + + let package_use = if package_module.is_empty() { + "crate::generated".to_string() + } else { + format!("crate::generated::{package_module}") + }; + writeln!(out, "use {package_use}::{{{server_module}}};").unwrap(); + writeln!( + out, + "use crate::server::{{GrpcDecode, GrpcEncode, GrpcInterface, GrpcServerStream, GrpcUnary, handle_streaming, handle_unary}};" + ) + .unwrap(); + writeln!(out).unwrap(); + + let mut unary_methods = Vec::new(); + let mut streaming_methods = Vec::new(); + + for method in &service.method { + let method_name = method.name.as_deref().unwrap_or("Method"); + let method_struct = format!("{method_name}Method"); + let request_type = proto_type_to_rust_path(method.input_type.as_deref().unwrap_or("")); + let response_type = proto_type_to_rust_path(method.output_type.as_deref().unwrap_or("")); + + if method.client_streaming() { + writeln!( + out, + "// NOTE: client-streaming and bidi methods are not generated ({method_name})." + ) + .unwrap(); + continue; + } + + writeln!(out, "pub struct {method_struct};").unwrap(); + writeln!(out).unwrap(); + writeln!(out, "impl GrpcInterface for {method_struct} {{").unwrap(); + writeln!(out, " type Request = {request_type};").unwrap(); + writeln!(out, " type Response = {response_type};").unwrap(); + writeln!(out, "}}").unwrap(); + writeln!(out).unwrap(); + + if method.server_streaming() { + streaming_methods.push((method_name.to_string(), method_struct)); + } else { + unary_methods.push((method_name.to_string(), method_struct)); + } + } + + let service_trait_name = format!("{service_name}Service"); + let mut trait_bounds = Vec::new(); + for (_, method_struct) in &unary_methods { + trait_bounds.push(format!("GrpcUnary<{method_struct}>")); + } + for (_, method_struct) in &streaming_methods { + trait_bounds.push(format!("GrpcServerStream<{method_struct}>")); + } + + if trait_bounds.is_empty() { + writeln!(out, "pub trait {service_trait_name} {{}}").unwrap(); + writeln!(out, "impl {service_trait_name} for T {{}}").unwrap(); + } else { + writeln!(out, "pub trait {service_trait_name}: {} {{}}", trait_bounds.join(" + ")).unwrap(); + writeln!( + out, + "impl {service_trait_name} for T where T: {} {{}}", + trait_bounds.join(" + ") + ) + .unwrap(); + } + writeln!(out).unwrap(); + + writeln!(out, "#[tonic::async_trait]").unwrap(); + writeln!(out, "impl {server_module}::{service_name} for T").unwrap(); + writeln!(out, "where").unwrap(); + writeln!(out, " T: {service_trait_name},").unwrap(); + + for (method_name, method_struct) in &unary_methods { + let request_type = proto_type_to_rust_path( + service + .method + .iter() + .find(|m| m.name.as_deref() == Some(method_name)) + .and_then(|m| m.input_type.as_deref()) + .unwrap_or(""), + ); + let response_type = proto_type_to_rust_path( + service + .method + .iter() + .find(|m| m.name.as_deref() == Some(method_name)) + .and_then(|m| m.output_type.as_deref()) + .unwrap_or(""), + ); + + writeln!(out, " {request_type}: GrpcDecode<>::Input>,") + .unwrap(); + writeln!( + out, + " >::Output: GrpcEncode<{response_type}>," + ) + .unwrap(); + } + + for (method_name, method_struct) in &streaming_methods { + let request_type = proto_type_to_rust_path( + service + .method + .iter() + .find(|m| m.name.as_deref() == Some(method_name)) + .and_then(|m| m.input_type.as_deref()) + .unwrap_or(""), + ); + writeln!( + out, + " {request_type}: GrpcDecode<>::Input>," + ) + .unwrap(); + } + + writeln!(out, "{{").unwrap(); + + for (method_name, method_struct) in &streaming_methods { + let stream_name = format!("{method_name}Stream"); + writeln!( + out, + " type {stream_name} = >::Stream;" + ) + .unwrap(); + } + + for (method_name, method_struct) in &unary_methods { + let method_fn = to_snake_case(method_name); + let request_type = proto_type_to_rust_path( + service + .method + .iter() + .find(|m| m.name.as_deref() == Some(method_name)) + .and_then(|m| m.input_type.as_deref()) + .unwrap_or(""), + ); + let response_type = proto_type_to_rust_path( + service + .method + .iter() + .find(|m| m.name.as_deref() == Some(method_name)) + .and_then(|m| m.output_type.as_deref()) + .unwrap_or(""), + ); + + writeln!( + out, + " async fn {method_fn}(&self, request: Request<{request_type}>) -> Result, Status> {{" + ) + .unwrap(); + writeln!(out, " handle_unary::<{method_struct}, _>(self, request).await").unwrap(); + writeln!(out, " }}").unwrap(); + } + + for (method_name, method_struct) in &streaming_methods { + let method_fn = to_snake_case(method_name); + let request_type = proto_type_to_rust_path( + service + .method + .iter() + .find(|m| m.name.as_deref() == Some(method_name)) + .and_then(|m| m.input_type.as_deref()) + .unwrap_or(""), + ); + let stream_name = format!("{method_name}Stream"); + + writeln!( + out, + " async fn {method_fn}(&self, request: Request<{request_type}>) -> Result, Status> {{" + ) + .unwrap(); + writeln!(out, " handle_streaming::<{method_struct}, _>(self, request).await") + .unwrap(); + writeln!(out, " }}").unwrap(); + } + + writeln!(out, "}}").unwrap(); + writeln!(out).unwrap(); + + writeln!(out, "pub struct {service_name}Server {{").unwrap(); + writeln!(out, " inner: {server_module}::{service_name}Server,").unwrap(); + writeln!(out, "}}").unwrap(); + writeln!(out).unwrap(); + + writeln!(out, "impl {service_name}Server").unwrap(); + writeln!(out, "where").unwrap(); + writeln!(out, " T: {service_trait_name},").unwrap(); + writeln!(out, "{{").unwrap(); + writeln!(out, " pub fn new(service: T) -> Self {{").unwrap(); + writeln!(out, " Self {{").unwrap(); + writeln!(out, " inner: {server_module}::{service_name}Server::new(service),") + .unwrap(); + writeln!(out, " }}").unwrap(); + writeln!(out, " }}").unwrap(); + writeln!(out, "}}").unwrap(); + writeln!(out).unwrap(); + + writeln!(out, "impl Clone for {service_name}Server {{").unwrap(); + writeln!(out, " fn clone(&self) -> Self {{").unwrap(); + writeln!(out, " Self {{ inner: self.inner.clone() }}").unwrap(); + writeln!(out, " }}").unwrap(); + writeln!(out, "}}").unwrap(); + writeln!(out).unwrap(); + + writeln!( + out, + "impl tonic::codegen::Service> for {service_name}Server" + ) + .unwrap(); + writeln!(out, "where").unwrap(); + writeln!( + out, + " {server_module}::{service_name}Server: tonic::codegen::Service>," + ) + .unwrap(); + writeln!(out, "{{").unwrap(); + writeln!( + out, + " type Response = <{server_module}::{service_name}Server as tonic::codegen::Service>>::Response;" + ) + .unwrap(); + writeln!( + out, + " type Error = <{server_module}::{service_name}Server as tonic::codegen::Service>>::Error;" + ) + .unwrap(); + writeln!( + out, + " type Future = <{server_module}::{service_name}Server as tonic::codegen::Service>>::Future;" + ) + .unwrap(); + writeln!(out).unwrap(); + writeln!( + out, + " fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> {{" + ) + .unwrap(); + writeln!(out, " self.inner.poll_ready(cx)").unwrap(); + writeln!(out, " }}").unwrap(); + writeln!(out).unwrap(); + writeln!(out, " fn call(&mut self, req: http::Request) -> Self::Future {{").unwrap(); + writeln!(out, " self.inner.call(req)").unwrap(); + writeln!(out, " }}").unwrap(); + writeln!(out, "}}").unwrap(); + writeln!(out).unwrap(); + + writeln!(out, "impl tonic::server::NamedService for {service_name}Server {{").unwrap(); + writeln!(out, " const NAME: &'static str = {server_module}::SERVICE_NAME;").unwrap(); + writeln!(out, "}}").unwrap(); + + out +} + +fn service_module_name(package: &str, service: &str) -> String { + if package.is_empty() { + to_snake_case(service) + } else { + format!("{package}_{}", to_snake_case(service)) + } +} + +fn to_snake_case(value: &str) -> String { + let mut out = String::new(); + for (idx, ch) in value.chars().enumerate() { + if ch.is_uppercase() { + if idx != 0 { + out.push('_'); + } + for lower in ch.to_lowercase() { + out.push(lower); + } + } else { + out.push(ch); + } + } + out +} + +fn proto_type_to_rust_path(proto_type: &str) -> String { + if proto_type == ".google.protobuf.Empty" { + return "()".to_string(); + } + + let trimmed = proto_type.trim_start_matches('.'); + let mut parts = trimmed.split('.').collect::>(); + if parts.is_empty() { + return "()".to_string(); + } + + let type_name = parts.pop().unwrap(); + let module_path = parts.join("::"); + if module_path.is_empty() { + format!("crate::generated::{type_name}") + } else { + format!("crate::generated::{module_path}::{type_name}") + } +} From a724ad8f67e75f343cbd3888ee091e0894651793 Mon Sep 17 00:00:00 2001 From: Mirko von Leipzig <48352201+Mirko-von-Leipzig@users.noreply.github.com> Date: Fri, 27 Feb 2026 13:05:07 +0200 Subject: [PATCH 05/24] Some codegen use --- Cargo.lock | 1 + crates/proto/Cargo.toml | 1 + crates/proto/build.rs | 309 ++++++++++++++++++++-------------------- 3 files changed, 156 insertions(+), 155 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0cea02269f..8cd326cf77 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3289,6 +3289,7 @@ dependencies = [ "anyhow", "assert_matches", "build-rs", + "codegen", "fs-err", "futures", "hex", diff --git a/crates/proto/Cargo.toml b/crates/proto/Cargo.toml index 1c3133013f..8a566ae485 100644 --- a/crates/proto/Cargo.toml +++ b/crates/proto/Cargo.toml @@ -35,6 +35,7 @@ proptest = { version = "1.7" } [build-dependencies] build-rs = { workspace = true } +codegen = { workspace = true } fs-err = { workspace = true } miden-node-proto-build = { features = ["internal"], workspace = true } miette = { version = "7.6" } diff --git a/crates/proto/build.rs b/crates/proto/build.rs index 021f3bb9e9..26a994b44d 100644 --- a/crates/proto/build.rs +++ b/crates/proto/build.rs @@ -1,6 +1,6 @@ -use std::fmt::Write; use std::path::Path; +use codegen::Scope; use fs_err as fs; use miden_node_proto_build::{ block_producer_api_descriptor, @@ -95,10 +95,12 @@ fn generate_mod_rs(dst_dir: impl AsRef) -> std::io::Result<()> { submodules.sort(); - let modules = submodules.iter().map(|f| format!("pub mod {f};\n")); - let contents = modules.into_iter().collect::(); + let mut scope = Scope::new(); + for module in submodules { + scope.raw(format!("pub mod {module};\n")); + } - fs::write(mod_filepath, contents) + fs::write(mod_filepath, scope.to_string()) } /// Generate server facade modules (one per service) from the provided descriptor sets. @@ -118,7 +120,8 @@ fn generate_server_modules( let server_module = format!("{}_server", to_snake_case(service_name)); let contents = - render_service_module(&package_module, service_name, &server_module, service); + render_service_module(&package_module, service_name, &server_module, service) + .to_string(); let path = dst_dir.join(format!("{module_name}.rs")); fs::write(path, contents).into_diagnostic().wrap_err("writing server module")?; @@ -135,26 +138,33 @@ fn render_service_module( service_name: &str, server_module: &str, service: &prost_types::ServiceDescriptorProto, -) -> String { - let mut out = String::new(); +) -> Scope { + let mut scope = Scope::new(); - writeln!(out, "use std::task::{{Context, Poll}};").unwrap(); - writeln!(out).unwrap(); - writeln!(out, "use tonic::{{Request, Response, Status}};").unwrap(); - writeln!(out).unwrap(); + scope.import("std::task", "Context"); + scope.import("std::task", "Poll"); + scope.import("tonic", "Request"); + scope.import("tonic", "Response"); + scope.import("tonic", "Status"); let package_use = if package_module.is_empty() { "crate::generated".to_string() } else { format!("crate::generated::{package_module}") }; - writeln!(out, "use {package_use}::{{{server_module}}};").unwrap(); - writeln!( - out, - "use crate::server::{{GrpcDecode, GrpcEncode, GrpcInterface, GrpcServerStream, GrpcUnary, handle_streaming, handle_unary}};" - ) - .unwrap(); - writeln!(out).unwrap(); + scope.import(&package_use, server_module); + + for import in [ + "GrpcDecode", + "GrpcEncode", + "GrpcInterface", + "GrpcServerStream", + "GrpcUnary", + "handle_streaming", + "handle_unary", + ] { + scope.import("crate::server", import); + } let mut unary_methods = Vec::new(); let mut streaming_methods = Vec::new(); @@ -166,21 +176,15 @@ fn render_service_module( let response_type = proto_type_to_rust_path(method.output_type.as_deref().unwrap_or("")); if method.client_streaming() { - writeln!( - out, - "// NOTE: client-streaming and bidi methods are not generated ({method_name})." - ) - .unwrap(); continue; } - writeln!(out, "pub struct {method_struct};").unwrap(); - writeln!(out).unwrap(); - writeln!(out, "impl GrpcInterface for {method_struct} {{").unwrap(); - writeln!(out, " type Request = {request_type};").unwrap(); - writeln!(out, " type Response = {response_type};").unwrap(); - writeln!(out, "}}").unwrap(); - writeln!(out).unwrap(); + scope.new_struct(&method_struct).vis("pub"); + + let method_impl = scope.new_impl(&method_struct); + method_impl.impl_trait("GrpcInterface"); + method_impl.associate_type("Request", request_type); + method_impl.associate_type("Response", response_type); if method.server_streaming() { streaming_methods.push((method_name.to_string(), method_struct)); @@ -198,24 +202,24 @@ fn render_service_module( trait_bounds.push(format!("GrpcServerStream<{method_struct}>")); } - if trait_bounds.is_empty() { - writeln!(out, "pub trait {service_trait_name} {{}}").unwrap(); - writeln!(out, "impl {service_trait_name} for T {{}}").unwrap(); - } else { - writeln!(out, "pub trait {service_trait_name}: {} {{}}", trait_bounds.join(" + ")).unwrap(); - writeln!( - out, - "impl {service_trait_name} for T where T: {} {{}}", - trait_bounds.join(" + ") - ) - .unwrap(); + let service_trait = scope.new_trait(&service_trait_name); + service_trait.vis("pub"); + for bound in &trait_bounds { + service_trait.parent(bound); + } + + let service_trait_impl = scope.new_impl("T"); + service_trait_impl.generic("T"); + service_trait_impl.impl_trait(&service_trait_name); + for bound in &trait_bounds { + service_trait_impl.bound("T", bound); } - writeln!(out).unwrap(); - writeln!(out, "#[tonic::async_trait]").unwrap(); - writeln!(out, "impl {server_module}::{service_name} for T").unwrap(); - writeln!(out, "where").unwrap(); - writeln!(out, " T: {service_trait_name},").unwrap(); + let service_impl = scope.new_impl("T"); + service_impl.generic("T"); + service_impl.impl_trait(format!("{server_module}::{service_name}")); + service_impl.r#macro("#[tonic::async_trait]"); + service_impl.bound("T", &service_trait_name); for (method_name, method_struct) in &unary_methods { let request_type = proto_type_to_rust_path( @@ -235,13 +239,12 @@ fn render_service_module( .unwrap_or(""), ); - writeln!(out, " {request_type}: GrpcDecode<>::Input>,") - .unwrap(); - writeln!( - out, - " >::Output: GrpcEncode<{response_type}>," - ) - .unwrap(); + service_impl + .bound(request_type, format!("GrpcDecode<>::Input>")); + service_impl.bound( + format!(">::Output"), + format!("GrpcEncode<{response_type}>"), + ); } for (method_name, method_struct) in &streaming_methods { @@ -253,22 +256,18 @@ fn render_service_module( .and_then(|m| m.input_type.as_deref()) .unwrap_or(""), ); - writeln!( - out, - " {request_type}: GrpcDecode<>::Input>," - ) - .unwrap(); + service_impl.bound( + request_type, + format!("GrpcDecode<>::Input>"), + ); } - writeln!(out, "{{").unwrap(); - for (method_name, method_struct) in &streaming_methods { let stream_name = format!("{method_name}Stream"); - writeln!( - out, - " type {stream_name} = >::Stream;" - ) - .unwrap(); + service_impl.associate_type( + stream_name, + format!(">::Stream"), + ); } for (method_name, method_struct) in &unary_methods { @@ -290,13 +289,12 @@ fn render_service_module( .unwrap_or(""), ); - writeln!( - out, - " async fn {method_fn}(&self, request: Request<{request_type}>) -> Result, Status> {{" - ) - .unwrap(); - writeln!(out, " handle_unary::<{method_struct}, _>(self, request).await").unwrap(); - writeln!(out, " }}").unwrap(); + let func = service_impl.new_fn(method_fn); + func.set_async(true); + func.arg_ref_self(); + func.arg("request", format!("Request<{request_type}>")); + func.ret(format!("Result, Status>")); + func.line(format!("handle_unary::<{method_struct}, _>(self, request).await")); } for (method_name, method_struct) in &streaming_methods { @@ -311,91 +309,92 @@ fn render_service_module( ); let stream_name = format!("{method_name}Stream"); - writeln!( - out, - " async fn {method_fn}(&self, request: Request<{request_type}>) -> Result, Status> {{" - ) - .unwrap(); - writeln!(out, " handle_streaming::<{method_struct}, _>(self, request).await") - .unwrap(); - writeln!(out, " }}").unwrap(); + let func = service_impl.new_fn(method_fn); + func.set_async(true); + func.arg_ref_self(); + func.arg("request", format!("Request<{request_type}>")); + func.ret(format!("Result, Status>")); + func.line(format!("handle_streaming::<{method_struct}, _>(self, request).await")); } - writeln!(out, "}}").unwrap(); - writeln!(out).unwrap(); - - writeln!(out, "pub struct {service_name}Server {{").unwrap(); - writeln!(out, " inner: {server_module}::{service_name}Server,").unwrap(); - writeln!(out, "}}").unwrap(); - writeln!(out).unwrap(); - - writeln!(out, "impl {service_name}Server").unwrap(); - writeln!(out, "where").unwrap(); - writeln!(out, " T: {service_trait_name},").unwrap(); - writeln!(out, "{{").unwrap(); - writeln!(out, " pub fn new(service: T) -> Self {{").unwrap(); - writeln!(out, " Self {{").unwrap(); - writeln!(out, " inner: {server_module}::{service_name}Server::new(service),") - .unwrap(); - writeln!(out, " }}").unwrap(); - writeln!(out, " }}").unwrap(); - writeln!(out, "}}").unwrap(); - writeln!(out).unwrap(); - - writeln!(out, "impl Clone for {service_name}Server {{").unwrap(); - writeln!(out, " fn clone(&self) -> Self {{").unwrap(); - writeln!(out, " Self {{ inner: self.inner.clone() }}").unwrap(); - writeln!(out, " }}").unwrap(); - writeln!(out, "}}").unwrap(); - writeln!(out).unwrap(); - - writeln!( - out, - "impl tonic::codegen::Service> for {service_name}Server" - ) - .unwrap(); - writeln!(out, "where").unwrap(); - writeln!( - out, - " {server_module}::{service_name}Server: tonic::codegen::Service>," - ) - .unwrap(); - writeln!(out, "{{").unwrap(); - writeln!( - out, - " type Response = <{server_module}::{service_name}Server as tonic::codegen::Service>>::Response;" - ) - .unwrap(); - writeln!( - out, - " type Error = <{server_module}::{service_name}Server as tonic::codegen::Service>>::Error;" - ) - .unwrap(); - writeln!( - out, - " type Future = <{server_module}::{service_name}Server as tonic::codegen::Service>>::Future;" - ) - .unwrap(); - writeln!(out).unwrap(); - writeln!( - out, - " fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> {{" - ) - .unwrap(); - writeln!(out, " self.inner.poll_ready(cx)").unwrap(); - writeln!(out, " }}").unwrap(); - writeln!(out).unwrap(); - writeln!(out, " fn call(&mut self, req: http::Request) -> Self::Future {{").unwrap(); - writeln!(out, " self.inner.call(req)").unwrap(); - writeln!(out, " }}").unwrap(); - writeln!(out, "}}").unwrap(); - writeln!(out).unwrap(); - - writeln!(out, "impl tonic::server::NamedService for {service_name}Server {{").unwrap(); - writeln!(out, " const NAME: &'static str = {server_module}::SERVICE_NAME;").unwrap(); - writeln!(out, "}}").unwrap(); - - out + let server_struct = scope.new_struct(format!("{service_name}Server")); + server_struct.vis("pub"); + server_struct.generic("T"); + server_struct.field("inner", format!("{server_module}::{service_name}Server")); + + let server_impl = scope.new_impl(format!("{service_name}Server")); + server_impl.generic("T"); + server_impl.target_generic("T"); + server_impl.bound("T", &service_trait_name); + let new_fn = server_impl.new_fn("new"); + new_fn.vis("pub"); + new_fn.arg("service", "T"); + new_fn.ret("Self"); + new_fn.line("Self {"); + new_fn.line(format!("inner: {server_module}::{service_name}Server::new(service),")); + new_fn.line("}"); + + let clone_impl = scope.new_impl(format!("{service_name}Server")); + clone_impl.generic("T"); + clone_impl.target_generic("T"); + clone_impl.impl_trait("Clone"); + let clone_fn = clone_impl.new_fn("clone"); + clone_fn.arg_ref_self(); + clone_fn.ret("Self"); + clone_fn.line("Self { inner: self.inner.clone() }"); + + let tonic_service_impl = scope.new_impl(format!("{service_name}Server")); + tonic_service_impl.generic("T"); + tonic_service_impl.generic("B"); + tonic_service_impl.target_generic("T"); + tonic_service_impl.impl_trait("tonic::codegen::Service>"); + tonic_service_impl.bound( + format!("{server_module}::{service_name}Server"), + "tonic::codegen::Service>", + ); + tonic_service_impl.associate_type( + "Response", + format!( + "<{server_module}::{service_name}Server as tonic::codegen::Service>>::Response" + ), + ); + tonic_service_impl.associate_type( + "Error", + format!( + "<{server_module}::{service_name}Server as tonic::codegen::Service>>::Error" + ), + ); + tonic_service_impl.associate_type( + "Future", + format!( + "<{server_module}::{service_name}Server as tonic::codegen::Service>>::Future" + ), + ); + + let poll_ready_fn = tonic_service_impl.new_fn("poll_ready"); + poll_ready_fn.arg_mut_self(); + poll_ready_fn.arg("cx", "&mut Context<'_>"); + poll_ready_fn.ret("Poll>"); + poll_ready_fn.line("self.inner.poll_ready(cx)"); + + let call_fn = tonic_service_impl.new_fn("call"); + call_fn.arg_mut_self(); + call_fn.arg("req", "http::Request"); + call_fn.ret("Self::Future"); + call_fn.line("self.inner.call(req)"); + + let named_service_impl = scope.new_impl(format!("{service_name}Server")); + named_service_impl.generic("T"); + named_service_impl.target_generic("T"); + named_service_impl.impl_trait("tonic::server::NamedService"); + named_service_impl.associate_const( + "NAME", + "&'static str", + format!("{server_module}::SERVICE_NAME"), + "", + ); + + scope } fn service_module_name(package: &str, service: &str) -> String { From 9a555d1e938550d5603727467dd2a9cd628373b0 Mon Sep 17 00:00:00 2001 From: Mirko von Leipzig <48352201+Mirko-von-Leipzig@users.noreply.github.com> Date: Fri, 27 Feb 2026 13:36:26 +0200 Subject: [PATCH 06/24] Appears to work --- crates/proto/build.rs | 336 ++++++++++++++++++++++++------------------ 1 file changed, 190 insertions(+), 146 deletions(-) diff --git a/crates/proto/build.rs b/crates/proto/build.rs index 26a994b44d..4b1fc2e75e 100644 --- a/crates/proto/build.rs +++ b/crates/proto/build.rs @@ -42,9 +42,10 @@ fn main() -> miette::Result<()> { .wrap_err("creating server destination folder")?; generate_server_modules(&descriptor_sets, &server_dst_dir)?; - generate_mod_rs(dst_dir) + generate_mod_rs(&server_dst_dir) .into_diagnostic() .wrap_err("generating server mod.rs")?; + generate_mod_rs(dst_dir).into_diagnostic().wrap_err("generating mod.rs")?; Ok(()) } @@ -152,7 +153,115 @@ fn render_service_module( } else { format!("crate::generated::{package_module}") }; - scope.import(&package_use, server_module); + add_server_imports(&mut scope, &package_use, server_module); + + let (unary_methods, streaming_methods) = collect_methods(&mut scope, service); + let service_trait_name = format!("{service_name}Service"); + let trait_bounds = build_trait_bounds(&unary_methods, &streaming_methods); + + let service_trait = scope.new_trait(&service_trait_name); + service_trait.vis("pub"); + apply_trait_bounds(service_trait, &trait_bounds); + + let service_trait_impl = scope.new_impl("T"); + service_trait_impl.generic("T"); + service_trait_impl.impl_trait(&service_trait_name); + apply_trait_impl_bounds(service_trait_impl, &trait_bounds); + + let service_impl = scope.new_impl("T"); + service_impl.generic("T"); + service_impl.impl_trait(format!("{server_module}::{service_name}")); + service_impl.r#macro("#[tonic::async_trait]"); + service_impl.bound("T", &service_trait_name); + + add_unary_bounds(service_impl, service, &unary_methods); + add_streaming_bounds(service_impl, service, &streaming_methods); + add_streaming_assoc_types(service_impl, &streaming_methods); + add_unary_methods(service_impl, service, &unary_methods); + add_streaming_methods(service_impl, service, &streaming_methods); + + let server_struct = scope.new_struct(format!("{service_name}Server")); + server_struct.vis("pub"); + server_struct.generic("T"); + server_struct.field("inner", format!("{server_module}::{service_name}Server")); + + let server_impl = scope.new_impl(format!("{service_name}Server")); + server_impl.generic("T"); + server_impl.target_generic("T"); + server_impl.bound("T", &service_trait_name); + let new_fn = server_impl.new_fn("new"); + new_fn.vis("pub"); + new_fn.arg("service", "T"); + new_fn.ret("Self"); + new_fn.line("Self {"); + new_fn.line(format!("inner: {server_module}::{service_name}Server::new(service),")); + new_fn.line("}"); + + let clone_impl = scope.new_impl(format!("{service_name}Server")); + clone_impl.generic("T"); + clone_impl.target_generic("T"); + clone_impl.impl_trait("Clone"); + let clone_fn = clone_impl.new_fn("clone"); + clone_fn.arg_ref_self(); + clone_fn.ret("Self"); + clone_fn.line("Self { inner: self.inner.clone() }"); + + let tonic_service_impl = scope.new_impl(format!("{service_name}Server")); + tonic_service_impl.generic("T"); + tonic_service_impl.generic("B"); + tonic_service_impl.target_generic("T"); + tonic_service_impl.impl_trait("tonic::codegen::Service>"); + tonic_service_impl.bound( + format!("{server_module}::{service_name}Server"), + "tonic::codegen::Service>", + ); + tonic_service_impl.associate_type( + "Response", + format!( + "<{server_module}::{service_name}Server as tonic::codegen::Service>>::Response" + ), + ); + tonic_service_impl.associate_type( + "Error", + format!( + "<{server_module}::{service_name}Server as tonic::codegen::Service>>::Error" + ), + ); + tonic_service_impl.associate_type( + "Future", + format!( + "<{server_module}::{service_name}Server as tonic::codegen::Service>>::Future" + ), + ); + + let poll_ready_fn = tonic_service_impl.new_fn("poll_ready"); + poll_ready_fn.arg_mut_self(); + poll_ready_fn.arg("cx", "&mut Context<'_>"); + poll_ready_fn.ret("Poll>"); + poll_ready_fn.line("self.inner.poll_ready(cx)"); + + let call_fn = tonic_service_impl.new_fn("call"); + call_fn.arg_mut_self(); + call_fn.arg("req", "http::Request"); + call_fn.ret("Self::Future"); + call_fn.line("self.inner.call(req)"); + + let named_service_impl = scope.new_impl(format!("{service_name}Server")); + named_service_impl.generic("T"); + named_service_impl.target_generic("T"); + named_service_impl.impl_trait("tonic::server::NamedService"); + named_service_impl.associate_const( + "NAME", + "&'static str", + format!("{server_module}::SERVICE_NAME"), + "", + ); + + scope +} + +fn add_server_imports(scope: &mut Scope, package_use: &str, server_module: &str) { + scope.import(package_use, server_module); for import in [ "GrpcDecode", @@ -165,7 +274,12 @@ fn render_service_module( ] { scope.import("crate::server", import); } +} +fn collect_methods( + scope: &mut Scope, + service: &prost_types::ServiceDescriptorProto, +) -> (Vec<(String, String)>, Vec<(String, String)>) { let mut unary_methods = Vec::new(); let mut streaming_methods = Vec::new(); @@ -193,51 +307,42 @@ fn render_service_module( } } - let service_trait_name = format!("{service_name}Service"); + (unary_methods, streaming_methods) +} + +fn build_trait_bounds( + unary_methods: &[(String, String)], + streaming_methods: &[(String, String)], +) -> Vec { let mut trait_bounds = Vec::new(); - for (_, method_struct) in &unary_methods { + for (_, method_struct) in unary_methods { trait_bounds.push(format!("GrpcUnary<{method_struct}>")); } - for (_, method_struct) in &streaming_methods { + for (_, method_struct) in streaming_methods { trait_bounds.push(format!("GrpcServerStream<{method_struct}>")); } + trait_bounds +} - let service_trait = scope.new_trait(&service_trait_name); - service_trait.vis("pub"); - for bound in &trait_bounds { +fn apply_trait_bounds(service_trait: &mut codegen::Trait, trait_bounds: &[String]) { + for bound in trait_bounds { service_trait.parent(bound); } +} - let service_trait_impl = scope.new_impl("T"); - service_trait_impl.generic("T"); - service_trait_impl.impl_trait(&service_trait_name); - for bound in &trait_bounds { +fn apply_trait_impl_bounds(service_trait_impl: &mut codegen::Impl, trait_bounds: &[String]) { + for bound in trait_bounds { service_trait_impl.bound("T", bound); } +} - let service_impl = scope.new_impl("T"); - service_impl.generic("T"); - service_impl.impl_trait(format!("{server_module}::{service_name}")); - service_impl.r#macro("#[tonic::async_trait]"); - service_impl.bound("T", &service_trait_name); - - for (method_name, method_struct) in &unary_methods { - let request_type = proto_type_to_rust_path( - service - .method - .iter() - .find(|m| m.name.as_deref() == Some(method_name)) - .and_then(|m| m.input_type.as_deref()) - .unwrap_or(""), - ); - let response_type = proto_type_to_rust_path( - service - .method - .iter() - .find(|m| m.name.as_deref() == Some(method_name)) - .and_then(|m| m.output_type.as_deref()) - .unwrap_or(""), - ); +fn add_unary_bounds( + service_impl: &mut codegen::Impl, + service: &prost_types::ServiceDescriptorProto, + unary_methods: &[(String, String)], +) { + for (method_name, method_struct) in unary_methods { + let (request_type, response_type) = method_types(service, method_name); service_impl .bound(request_type, format!("GrpcDecode<>::Input>")); @@ -246,48 +351,43 @@ fn render_service_module( format!("GrpcEncode<{response_type}>"), ); } +} - for (method_name, method_struct) in &streaming_methods { - let request_type = proto_type_to_rust_path( - service - .method - .iter() - .find(|m| m.name.as_deref() == Some(method_name)) - .and_then(|m| m.input_type.as_deref()) - .unwrap_or(""), - ); +fn add_streaming_bounds( + service_impl: &mut codegen::Impl, + service: &prost_types::ServiceDescriptorProto, + streaming_methods: &[(String, String)], +) { + for (method_name, method_struct) in streaming_methods { + let (request_type, _) = method_types(service, method_name); service_impl.bound( request_type, format!("GrpcDecode<>::Input>"), ); } +} - for (method_name, method_struct) in &streaming_methods { +fn add_streaming_assoc_types( + service_impl: &mut codegen::Impl, + streaming_methods: &[(String, String)], +) { + for (method_name, method_struct) in streaming_methods { let stream_name = format!("{method_name}Stream"); service_impl.associate_type( stream_name, format!(">::Stream"), ); } +} - for (method_name, method_struct) in &unary_methods { +fn add_unary_methods( + service_impl: &mut codegen::Impl, + service: &prost_types::ServiceDescriptorProto, + unary_methods: &[(String, String)], +) { + for (method_name, method_struct) in unary_methods { let method_fn = to_snake_case(method_name); - let request_type = proto_type_to_rust_path( - service - .method - .iter() - .find(|m| m.name.as_deref() == Some(method_name)) - .and_then(|m| m.input_type.as_deref()) - .unwrap_or(""), - ); - let response_type = proto_type_to_rust_path( - service - .method - .iter() - .find(|m| m.name.as_deref() == Some(method_name)) - .and_then(|m| m.output_type.as_deref()) - .unwrap_or(""), - ); + let (request_type, response_type) = method_types(service, method_name); let func = service_impl.new_fn(method_fn); func.set_async(true); @@ -296,17 +396,16 @@ fn render_service_module( func.ret(format!("Result, Status>")); func.line(format!("handle_unary::<{method_struct}, _>(self, request).await")); } +} - for (method_name, method_struct) in &streaming_methods { +fn add_streaming_methods( + service_impl: &mut codegen::Impl, + service: &prost_types::ServiceDescriptorProto, + streaming_methods: &[(String, String)], +) { + for (method_name, method_struct) in streaming_methods { let method_fn = to_snake_case(method_name); - let request_type = proto_type_to_rust_path( - service - .method - .iter() - .find(|m| m.name.as_deref() == Some(method_name)) - .and_then(|m| m.input_type.as_deref()) - .unwrap_or(""), - ); + let (request_type, _) = method_types(service, method_name); let stream_name = format!("{method_name}Stream"); let func = service_impl.new_fn(method_fn); @@ -316,85 +415,30 @@ fn render_service_module( func.ret(format!("Result, Status>")); func.line(format!("handle_streaming::<{method_struct}, _>(self, request).await")); } +} - let server_struct = scope.new_struct(format!("{service_name}Server")); - server_struct.vis("pub"); - server_struct.generic("T"); - server_struct.field("inner", format!("{server_module}::{service_name}Server")); - - let server_impl = scope.new_impl(format!("{service_name}Server")); - server_impl.generic("T"); - server_impl.target_generic("T"); - server_impl.bound("T", &service_trait_name); - let new_fn = server_impl.new_fn("new"); - new_fn.vis("pub"); - new_fn.arg("service", "T"); - new_fn.ret("Self"); - new_fn.line("Self {"); - new_fn.line(format!("inner: {server_module}::{service_name}Server::new(service),")); - new_fn.line("}"); - - let clone_impl = scope.new_impl(format!("{service_name}Server")); - clone_impl.generic("T"); - clone_impl.target_generic("T"); - clone_impl.impl_trait("Clone"); - let clone_fn = clone_impl.new_fn("clone"); - clone_fn.arg_ref_self(); - clone_fn.ret("Self"); - clone_fn.line("Self { inner: self.inner.clone() }"); - - let tonic_service_impl = scope.new_impl(format!("{service_name}Server")); - tonic_service_impl.generic("T"); - tonic_service_impl.generic("B"); - tonic_service_impl.target_generic("T"); - tonic_service_impl.impl_trait("tonic::codegen::Service>"); - tonic_service_impl.bound( - format!("{server_module}::{service_name}Server"), - "tonic::codegen::Service>", - ); - tonic_service_impl.associate_type( - "Response", - format!( - "<{server_module}::{service_name}Server as tonic::codegen::Service>>::Response" - ), - ); - tonic_service_impl.associate_type( - "Error", - format!( - "<{server_module}::{service_name}Server as tonic::codegen::Service>>::Error" - ), - ); - tonic_service_impl.associate_type( - "Future", - format!( - "<{server_module}::{service_name}Server as tonic::codegen::Service>>::Future" - ), +fn method_types( + service: &prost_types::ServiceDescriptorProto, + method_name: &str, +) -> (String, String) { + let request_type = proto_type_to_rust_path( + service + .method + .iter() + .find(|m| m.name.as_deref() == Some(method_name)) + .and_then(|m| m.input_type.as_deref()) + .unwrap_or(""), ); - - let poll_ready_fn = tonic_service_impl.new_fn("poll_ready"); - poll_ready_fn.arg_mut_self(); - poll_ready_fn.arg("cx", "&mut Context<'_>"); - poll_ready_fn.ret("Poll>"); - poll_ready_fn.line("self.inner.poll_ready(cx)"); - - let call_fn = tonic_service_impl.new_fn("call"); - call_fn.arg_mut_self(); - call_fn.arg("req", "http::Request"); - call_fn.ret("Self::Future"); - call_fn.line("self.inner.call(req)"); - - let named_service_impl = scope.new_impl(format!("{service_name}Server")); - named_service_impl.generic("T"); - named_service_impl.target_generic("T"); - named_service_impl.impl_trait("tonic::server::NamedService"); - named_service_impl.associate_const( - "NAME", - "&'static str", - format!("{server_module}::SERVICE_NAME"), - "", + let response_type = proto_type_to_rust_path( + service + .method + .iter() + .find(|m| m.name.as_deref() == Some(method_name)) + .and_then(|m| m.output_type.as_deref()) + .unwrap_or(""), ); - scope + (request_type, response_type) } fn service_module_name(package: &str, service: &str) -> String { From 98a183d76e1bad029adddcacfc1d407019addede Mon Sep 17 00:00:00 2001 From: Mirko von Leipzig <48352201+Mirko-von-Leipzig@users.noreply.github.com> Date: Mon, 2 Mar 2026 16:27:23 +0200 Subject: [PATCH 07/24] Simplify --- crates/proto/build.rs | 526 ++++++++++++++---------------------------- 1 file changed, 177 insertions(+), 349 deletions(-) diff --git a/crates/proto/build.rs b/crates/proto/build.rs index 4b1fc2e75e..a77a552414 100644 --- a/crates/proto/build.rs +++ b/crates/proto/build.rs @@ -1,6 +1,7 @@ use std::path::Path; +use std::process::Command; -use codegen::Scope; +use codegen::{Scope, *}; use fs_err as fs; use miden_node_proto_build::{ block_producer_api_descriptor, @@ -11,6 +12,7 @@ use miden_node_proto_build::{ validator_api_descriptor, }; use miette::{Context, IntoDiagnostic}; +use prost_types::{MethodDescriptorProto, ServiceDescriptorProto}; use tonic_prost_build::FileDescriptorSet; /// Generates Rust protobuf bindings using `miden-node-proto-build`. @@ -42,11 +44,15 @@ fn main() -> miette::Result<()> { .wrap_err("creating server destination folder")?; generate_server_modules(&descriptor_sets, &server_dst_dir)?; + generate_mod_rs(&server_dst_dir) .into_diagnostic() .wrap_err("generating server mod.rs")?; - generate_mod_rs(dst_dir).into_diagnostic().wrap_err("generating mod.rs")?; + // generate_server_modules(&descriptor_sets, &server_dst_dir)?; + generate_mod_rs(&dst_dir).into_diagnostic().wrap_err("generating mod.rs")?; + + rustfmt_generated(&dst_dir)?; Ok(()) } @@ -66,42 +72,60 @@ fn generate_bindings(file_descriptors: FileDescriptorSet, dst_dir: &Path) -> mie Ok(()) } +fn rustfmt_generated(dir: &Path) -> miette::Result<()> { + let mut rs_files = Vec::new(); + collect_rs_files(dir, &mut rs_files)?; + + if rs_files.is_empty() { + return Ok(()); + } + + let status = Command::new("rustfmt") + .args(&rs_files) + .status() + .into_diagnostic() + .wrap_err("running rustfmt on generated files")?; + + if !status.success() { + miette::bail!("rustfmt failed with status: {status}"); + } + + Ok(()) +} + +fn collect_rs_files(dir: &Path, out: &mut Vec) -> miette::Result<()> { + for entry in fs_err::read_dir(dir).into_diagnostic()? { + let entry = entry.into_diagnostic()?; + let path = entry.path(); + if path.is_dir() { + collect_rs_files(&path, out)?; + } else if path.extension().is_some_and(|ext| ext == "rs") { + out.push(path); + } + } + Ok(()) +} + /// Generate `mod.rs` which includes all files in the folder as submodules. fn generate_mod_rs(dst_dir: impl AsRef) -> std::io::Result<()> { - let mod_filepath = dst_dir.as_ref().join("mod.rs"); + let mut scope = Scope::new(); - // Discover all submodules by iterating over the folder contents. - let mut submodules = Vec::new(); for entry in fs::read_dir(dst_dir.as_ref())? { let entry = entry?; let path = entry.path(); - if path.is_file() { - let file_stem = path - .file_stem() - .and_then(|f| f.to_str()) - .expect("Could not get file name") - .to_owned(); - - submodules.push(file_stem); - } else if path.is_dir() { - let dir_name = path - .file_name() - .and_then(|f| f.to_str()) - .expect("Could not get directory name") - .to_owned(); - - submodules.push(dir_name); - } - } - submodules.sort(); + let name = if path.is_file() { + path.file_stem().and_then(|f| f.to_str()).expect("Could not get file name") + } else if path.is_dir() { + path.file_name().and_then(|f| f.to_str()).expect("Could not get directory name") + } else { + continue; + }; - let mut scope = Scope::new(); - for module in submodules { - scope.raw(format!("pub mod {module};\n")); + scope.raw(format!("pub mod {name};")); } - fs::write(mod_filepath, scope.to_string()) + fs::write(dst_dir.as_ref().join("mod.rs"), scope.to_string()) } /// Generate server facade modules (one per service) from the provided descriptor sets. @@ -112,17 +136,14 @@ fn generate_server_modules( for fds in descriptor_sets { for file in &fds.file { let package = file.package.as_deref().unwrap_or_default(); - let package_module = package.replace('.', "::"); - let package_prefix = package.replace('.', "_"); + let package = package.replace('.', "_"); for service in &file.service { let service_name = service.name.as_deref().unwrap_or("Service"); - let module_name = service_module_name(&package_prefix, service_name); - let server_module = format!("{}_server", to_snake_case(service_name)); + let service_name = to_snake_case(service_name); + let module_name = format!("{}_{}", &package, service_name); - let contents = - render_service_module(&package_module, service_name, &server_module, service) - .to_string(); + let contents = Service::from_descriptor(service).generate().scope().to_string(); let path = dst_dir.join(format!("{module_name}.rs")); fs::write(path, contents).into_diagnostic().wrap_err("writing server module")?; @@ -133,355 +154,162 @@ fn generate_server_modules( Ok(()) } -#[expect(clippy::too_many_lines, reason = "Will split later")] -fn render_service_module( - package_module: &str, - service_name: &str, - server_module: &str, - service: &prost_types::ServiceDescriptorProto, -) -> Scope { - let mut scope = Scope::new(); - - scope.import("std::task", "Context"); - scope.import("std::task", "Poll"); - scope.import("tonic", "Request"); - scope.import("tonic", "Response"); - scope.import("tonic", "Status"); - - let package_use = if package_module.is_empty() { - "crate::generated".to_string() - } else { - format!("crate::generated::{package_module}") - }; - add_server_imports(&mut scope, &package_use, server_module); - - let (unary_methods, streaming_methods) = collect_methods(&mut scope, service); - let service_trait_name = format!("{service_name}Service"); - let trait_bounds = build_trait_bounds(&unary_methods, &streaming_methods); - - let service_trait = scope.new_trait(&service_trait_name); - service_trait.vis("pub"); - apply_trait_bounds(service_trait, &trait_bounds); - - let service_trait_impl = scope.new_impl("T"); - service_trait_impl.generic("T"); - service_trait_impl.impl_trait(&service_trait_name); - apply_trait_impl_bounds(service_trait_impl, &trait_bounds); - - let service_impl = scope.new_impl("T"); - service_impl.generic("T"); - service_impl.impl_trait(format!("{server_module}::{service_name}")); - service_impl.r#macro("#[tonic::async_trait]"); - service_impl.bound("T", &service_trait_name); - - add_unary_bounds(service_impl, service, &unary_methods); - add_streaming_bounds(service_impl, service, &streaming_methods); - add_streaming_assoc_types(service_impl, &streaming_methods); - add_unary_methods(service_impl, service, &unary_methods); - add_streaming_methods(service_impl, service, &streaming_methods); - - let server_struct = scope.new_struct(format!("{service_name}Server")); - server_struct.vis("pub"); - server_struct.generic("T"); - server_struct.field("inner", format!("{server_module}::{service_name}Server")); - - let server_impl = scope.new_impl(format!("{service_name}Server")); - server_impl.generic("T"); - server_impl.target_generic("T"); - server_impl.bound("T", &service_trait_name); - let new_fn = server_impl.new_fn("new"); - new_fn.vis("pub"); - new_fn.arg("service", "T"); - new_fn.ret("Self"); - new_fn.line("Self {"); - new_fn.line(format!("inner: {server_module}::{service_name}Server::new(service),")); - new_fn.line("}"); - - let clone_impl = scope.new_impl(format!("{service_name}Server")); - clone_impl.generic("T"); - clone_impl.target_generic("T"); - clone_impl.impl_trait("Clone"); - let clone_fn = clone_impl.new_fn("clone"); - clone_fn.arg_ref_self(); - clone_fn.ret("Self"); - clone_fn.line("Self { inner: self.inner.clone() }"); - - let tonic_service_impl = scope.new_impl(format!("{service_name}Server")); - tonic_service_impl.generic("T"); - tonic_service_impl.generic("B"); - tonic_service_impl.target_generic("T"); - tonic_service_impl.impl_trait("tonic::codegen::Service>"); - tonic_service_impl.bound( - format!("{server_module}::{service_name}Server"), - "tonic::codegen::Service>", - ); - tonic_service_impl.associate_type( - "Response", - format!( - "<{server_module}::{service_name}Server as tonic::codegen::Service>>::Response" - ), - ); - tonic_service_impl.associate_type( - "Error", - format!( - "<{server_module}::{service_name}Server as tonic::codegen::Service>>::Error" - ), - ); - tonic_service_impl.associate_type( - "Future", - format!( - "<{server_module}::{service_name}Server as tonic::codegen::Service>>::Future" - ), - ); - - let poll_ready_fn = tonic_service_impl.new_fn("poll_ready"); - poll_ready_fn.arg_mut_self(); - poll_ready_fn.arg("cx", "&mut Context<'_>"); - poll_ready_fn.ret("Poll>"); - poll_ready_fn.line("self.inner.poll_ready(cx)"); - - let call_fn = tonic_service_impl.new_fn("call"); - call_fn.arg_mut_self(); - call_fn.arg("req", "http::Request"); - call_fn.ret("Self::Future"); - call_fn.line("self.inner.call(req)"); - - let named_service_impl = scope.new_impl(format!("{service_name}Server")); - named_service_impl.generic("T"); - named_service_impl.target_generic("T"); - named_service_impl.impl_trait("tonic::server::NamedService"); - named_service_impl.associate_const( - "NAME", - "&'static str", - format!("{server_module}::SERVICE_NAME"), - "", - ); - - scope +struct Service { + name: String, + methods: Vec, } -fn add_server_imports(scope: &mut Scope, package_use: &str, server_module: &str) { - scope.import(package_use, server_module); - - for import in [ - "GrpcDecode", - "GrpcEncode", - "GrpcInterface", - "GrpcServerStream", - "GrpcUnary", - "handle_streaming", - "handle_unary", - ] { - scope.import("crate::server", import); - } +struct Method { + name: String, + request: String, + response: String, } -fn collect_methods( - scope: &mut Scope, - service: &prost_types::ServiceDescriptorProto, -) -> (Vec<(String, String)>, Vec<(String, String)>) { - let mut unary_methods = Vec::new(); - let mut streaming_methods = Vec::new(); +impl Service { + fn from_descriptor(descriptor: &ServiceDescriptorProto) -> Self { + let name = descriptor.name().to_string(); + let methods = descriptor.method.iter().map(Method::from_descriptor).collect(); - for method in &service.method { - let method_name = method.name.as_deref().unwrap_or("Method"); - let method_struct = format!("{method_name}Method"); - let request_type = proto_type_to_rust_path(method.input_type.as_deref().unwrap_or("")); - let response_type = proto_type_to_rust_path(method.output_type.as_deref().unwrap_or("")); + Self { name, methods } + } - if method.client_streaming() { - continue; - } + /// Generates a module containing the service's interface and implementation, including the + /// methods. + fn generate(&self) -> Module { + let mut module = Module::new(&self.name); - scope.new_struct(&method_struct).vis("pub"); + module.import("crate::server", "GrpcInterface"); + module.import("crate::server", "GrpcUnary"); + module.import("crate::server", "handle_unary"); - let method_impl = scope.new_impl(&method_struct); - method_impl.impl_trait("GrpcInterface"); - method_impl.associate_type("Request", request_type); - method_impl.associate_type("Response", response_type); + module.push_trait(self.service_trait()); + module.push_impl(self.blanket_impl()); - if method.server_streaming() { - streaming_methods.push((method_name.to_string(), method_struct)); - } else { - unary_methods.push((method_name.to_string(), method_struct)); + for method in &self.methods { + module.push_struct(method.marker_struct()); + module.push_impl(method.grpc_interface_impl()); } + + module } - (unary_methods, streaming_methods) -} + /// The trait describing the service's interface. + /// + /// This is a super trait consisting of all the gRPC method traits for this service. + /// + /// ```rust + /// trait : + /// GrpcUnary + + /// GrpcUnary + + /// ... + /// GrpcUnary, + /// {} + /// ``` + fn service_trait(&self) -> Trait { + let mut ret = Trait::new(format!("{}Service", &self.name)); + ret.vis("pub"); + + for method in &self.methods { + ret.parent(method.unary_trait().ty()); + } -fn build_trait_bounds( - unary_methods: &[(String, String)], - streaming_methods: &[(String, String)], -) -> Vec { - let mut trait_bounds = Vec::new(); - for (_, method_struct) in unary_methods { - trait_bounds.push(format!("GrpcUnary<{method_struct}>")); - } - for (_, method_struct) in streaming_methods { - trait_bounds.push(format!("GrpcServerStream<{method_struct}>")); + ret } - trait_bounds -} -fn apply_trait_bounds(service_trait: &mut codegen::Trait, trait_bounds: &[String]) { - for bound in trait_bounds { - service_trait.parent(bound); - } -} + /// The blanket implementation of the the service's trait, for all `T` that implement all + /// required gRPC methods. + /// + /// ```rust + /// impl for T + /// where T: + /// GrpcUnary + + /// GrpcUnary + + /// ... + /// GrpcUnary, + /// {} + /// ``` + fn blanket_impl(&self) -> Impl { + let mut ret = Impl::new("T"); + ret.generic("T").impl_trait(self.service_trait().ty()); + + for method in &self.methods { + ret.bound("T", method.unary_trait().ty()); + } -fn apply_trait_impl_bounds(service_trait_impl: &mut codegen::Impl, trait_bounds: &[String]) { - for bound in trait_bounds { - service_trait_impl.bound("T", bound); + ret } } -fn add_unary_bounds( - service_impl: &mut codegen::Impl, - service: &prost_types::ServiceDescriptorProto, - unary_methods: &[(String, String)], -) { - for (method_name, method_struct) in unary_methods { - let (request_type, response_type) = method_types(service, method_name); - - service_impl - .bound(request_type, format!("GrpcDecode<>::Input>")); - service_impl.bound( - format!(">::Output"), - format!("GrpcEncode<{response_type}>"), - ); - } -} +impl Method { + fn from_descriptor(descriptor: &MethodDescriptorProto) -> Self { + let name = descriptor.name().to_string(); -fn add_streaming_bounds( - service_impl: &mut codegen::Impl, - service: &prost_types::ServiceDescriptorProto, - streaming_methods: &[(String, String)], -) { - for (method_name, method_struct) in streaming_methods { - let (request_type, _) = method_types(service, method_name); - service_impl.bound( - request_type, - format!("GrpcDecode<>::Input>"), - ); - } -} + let request = Self::grpc_path_to_generated(descriptor.input_type()); + let response = Self::grpc_path_to_generated(descriptor.output_type()); -fn add_streaming_assoc_types( - service_impl: &mut codegen::Impl, - streaming_methods: &[(String, String)], -) { - for (method_name, method_struct) in streaming_methods { - let stream_name = format!("{method_name}Stream"); - service_impl.associate_type( - stream_name, - format!(">::Stream"), - ); + Self { name, request, response } } -} -fn add_unary_methods( - service_impl: &mut codegen::Impl, - service: &prost_types::ServiceDescriptorProto, - unary_methods: &[(String, String)], -) { - for (method_name, method_struct) in unary_methods { - let method_fn = to_snake_case(method_name); - let (request_type, response_type) = method_types(service, method_name); - - let func = service_impl.new_fn(method_fn); - func.set_async(true); - func.arg_ref_self(); - func.arg("request", format!("Request<{request_type}>")); - func.ret(format!("Result, Status>")); - func.line(format!("handle_unary::<{method_struct}, _>(self, request).await")); + /// This [`Method`]'s marker struct. + /// + /// ```rust + /// pub struct ; + /// ``` + fn marker_struct(&self) -> Struct { + let mut ret = Struct::new(&self.name); + ret.vis("pub"); + ret } -} -fn add_streaming_methods( - service_impl: &mut codegen::Impl, - service: &prost_types::ServiceDescriptorProto, - streaming_methods: &[(String, String)], -) { - for (method_name, method_struct) in streaming_methods { - let method_fn = to_snake_case(method_name); - let (request_type, _) = method_types(service, method_name); - let stream_name = format!("{method_name}Stream"); - - let func = service_impl.new_fn(method_fn); - func.set_async(true); - func.arg_ref_self(); - func.arg("request", format!("Request<{request_type}>")); - func.ret(format!("Result, Status>")); - func.line(format!("handle_streaming::<{method_struct}, _>(self, request).await")); + /// Returns this method's unary trait concrete type. + /// + /// ```rust + /// GrpcUnary + /// ``` + fn unary_trait(&self) -> Trait { + let mut ret = Trait::new("GrpcUnary"); + ret.generic(&self.name); + ret } -} -fn method_types( - service: &prost_types::ServiceDescriptorProto, - method_name: &str, -) -> (String, String) { - let request_type = proto_type_to_rust_path( - service - .method - .iter() - .find(|m| m.name.as_deref() == Some(method_name)) - .and_then(|m| m.input_type.as_deref()) - .unwrap_or(""), - ); - let response_type = proto_type_to_rust_path( - service - .method - .iter() - .find(|m| m.name.as_deref() == Some(method_name)) - .and_then(|m| m.output_type.as_deref()) - .unwrap_or(""), - ); - - (request_type, response_type) -} + /// This method's implementation of the `GrpcInterface` trait. + fn grpc_interface_impl(&self) -> Impl { + let mut ret = Impl::new(&self.name); + ret.impl_trait("GrpcInterface") + .associate_type("Request", &self.request) + .associate_type("Response", &self.response); -fn service_module_name(package: &str, service: &str) -> String { - if package.is_empty() { - to_snake_case(service) - } else { - format!("{package}_{}", to_snake_case(service)) + ret } -} -fn to_snake_case(value: &str) -> String { - let mut out = String::new(); - for (idx, ch) in value.chars().enumerate() { - if ch.is_uppercase() { - if idx != 0 { - out.push('_'); - } - for lower in ch.to_lowercase() { - out.push(lower); - } - } else { - out.push(ch); + /// Translates a gRPC protobuf path to the corresponding generated Rust path. This is used to + /// translate the protobuf type definitions to their tonic generated Rust types. + /// + /// i.e. `.x.y.z` -> `crate::generated::x::y::z` + /// + /// It also handles the case where the path is `.google.protobuf.Empty` by returning `()`. + fn grpc_path_to_generated(path: &str) -> String { + if path == ".google.protobuf.Empty" { + return "()".to_string(); } + + let path = path.trim_start_matches('.').replace('.', "::"); + format!("crate::generated::{path}") } - out } -fn proto_type_to_rust_path(proto_type: &str) -> String { - if proto_type == ".google.protobuf.Empty" { - return "()".to_string(); - } +/// Converts a string to snake_case. +fn to_snake_case(s: &str) -> String { + let mut ret = String::new(); - let trimmed = proto_type.trim_start_matches('.'); - let mut parts = trimmed.split('.').collect::>(); - if parts.is_empty() { - return "()".to_string(); + for c in s.chars() { + if c.is_uppercase() { + if !ret.is_empty() { + ret.push('_'); + } + } + ret.push(c.to_ascii_lowercase()); } - let type_name = parts.pop().unwrap(); - let module_path = parts.join("::"); - if module_path.is_empty() { - format!("crate::generated::{type_name}") - } else { - format!("crate::generated::{module_path}::{type_name}") - } + ret } From be59320cb1c7f33b99b7614cdeb20f39927fec30 Mon Sep 17 00:00:00 2001 From: Mirko von Leipzig <48352201+Mirko-von-Leipzig@users.noreply.github.com> Date: Mon, 2 Mar 2026 18:12:30 +0200 Subject: [PATCH 08/24] Partial stream success --- crates/proto/build.rs | 173 +++++++++++++++++++---- crates/proto/src/server/mod.rs | 12 +- crates/proto/src/server/remote_prover.rs | 74 ---------- 3 files changed, 146 insertions(+), 113 deletions(-) delete mode 100644 crates/proto/src/server/remote_prover.rs diff --git a/crates/proto/build.rs b/crates/proto/build.rs index a77a552414..5776095e95 100644 --- a/crates/proto/build.rs +++ b/crates/proto/build.rs @@ -1,7 +1,7 @@ use std::path::Path; use std::process::Command; -use codegen::{Scope, *}; +use codegen::{Function, Impl, Module, Struct, Trait, Type}; use fs_err as fs; use miden_node_proto_build::{ block_producer_api_descriptor, @@ -108,13 +108,14 @@ fn collect_rs_files(dir: &Path, out: &mut Vec) -> miette::Re /// Generate `mod.rs` which includes all files in the folder as submodules. fn generate_mod_rs(dst_dir: impl AsRef) -> std::io::Result<()> { - let mut scope = Scope::new(); + // I couldn't find any `codegen::` function for `mod ;`, so we generate it manually. + let mut modules = Vec::new(); for entry in fs::read_dir(dst_dir.as_ref())? { let entry = entry?; let path = entry.path(); - let name = if path.is_file() { + let module = if path.is_file() { path.file_stem().and_then(|f| f.to_str()).expect("Could not get file name") } else if path.is_dir() { path.file_name().and_then(|f| f.to_str()).expect("Could not get directory name") @@ -122,10 +123,10 @@ fn generate_mod_rs(dst_dir: impl AsRef) -> std::io::Result<()> { continue; }; - scope.raw(format!("pub mod {name};")); + modules.push(format!("pub mod {module};")); } - fs::write(dst_dir.as_ref().join("mod.rs"), scope.to_string()) + fs::write(dst_dir.as_ref().join("mod.rs"), modules.join("\n")) } /// Generate server facade modules (one per service) from the provided descriptor sets. @@ -143,7 +144,8 @@ fn generate_server_modules( let service_name = to_snake_case(service_name); let module_name = format!("{}_{}", &package, service_name); - let contents = Service::from_descriptor(service).generate().scope().to_string(); + let contents = + Service::from_descriptor(service, &package).generate().scope().to_string(); let path = dst_dir.join(format!("{module_name}.rs")); fs::write(path, contents).into_diagnostic().wrap_err("writing server module")?; @@ -156,21 +158,52 @@ fn generate_server_modules( struct Service { name: String, - methods: Vec, + package: String, + unary_methods: Vec, + server_streams: Vec, } -struct Method { +struct UnaryMethod { + name: String, + request: String, + response: String, +} + +struct ServerStream { name: String, request: String, response: String, } impl Service { - fn from_descriptor(descriptor: &ServiceDescriptorProto) -> Self { + fn from_descriptor(descriptor: &ServiceDescriptorProto, package: &str) -> Self { let name = descriptor.name().to_string(); - let methods = descriptor.method.iter().map(Method::from_descriptor).collect(); - - Self { name, methods } + let unary_methods = descriptor + .method + .iter() + .filter(|method| !method.client_streaming() && !method.server_streaming()) + .map(UnaryMethod::from_descriptor) + .collect(); + let server_streams = descriptor + .method + .iter() + .filter(|method| method.server_streaming()) + .map(ServerStream::from_descriptor) + .collect(); + let package = package.to_string(); + + // We don't have any client streams, so no need to support them. + assert!( + !descriptor.method.iter().any(MethodDescriptorProto::client_streaming), + "client streams are not supported" + ); + + Self { + name, + package, + unary_methods, + server_streams, + } } /// Generates a module containing the service's interface and implementation, including the @@ -184,12 +217,17 @@ impl Service { module.push_trait(self.service_trait()); module.push_impl(self.blanket_impl()); + module.push_impl(self.tonic_impl()); - for method in &self.methods { + for method in &self.unary_methods { module.push_struct(method.marker_struct()); module.push_impl(method.grpc_interface_impl()); } + for stream in &self.server_streams { + module.push_struct(stream.marker_struct()); + } + module } @@ -209,7 +247,7 @@ impl Service { let mut ret = Trait::new(format!("{}Service", &self.name)); ret.vis("pub"); - for method in &self.methods { + for method in &self.unary_methods { ret.parent(method.unary_trait().ty()); } @@ -232,20 +270,41 @@ impl Service { let mut ret = Impl::new("T"); ret.generic("T").impl_trait(self.service_trait().ty()); - for method in &self.methods { + for method in &self.unary_methods { ret.bound("T", method.unary_trait().ty()); } ret } + + fn tonic_impl(&self) -> Impl { + let tonic_path = format!("crate::generated::{}::api_server::{}", self.package, self.name); + + let mut ret = Impl::new("T"); + ret.generic("T") + .bound("T", self.service_trait().ty()) + .impl_trait(tonic_path) + .r#macro("#[tonic::async_trait]"); + + for method in &self.unary_methods { + ret.push_fn(method.tonic_impl()); + } + + for stream in &self.server_streams { + ret.push_fn(stream.tonic_impl()); + ret.associate_type(stream.associated_type().0, stream.associated_type().1); + } + + ret + } } -impl Method { +impl UnaryMethod { fn from_descriptor(descriptor: &MethodDescriptorProto) -> Self { let name = descriptor.name().to_string(); - let request = Self::grpc_path_to_generated(descriptor.input_type()); - let response = Self::grpc_path_to_generated(descriptor.output_type()); + let request = grpc_path_to_generated(descriptor.input_type()); + let response = grpc_path_to_generated(descriptor.output_type()); Self { name, request, response } } @@ -282,23 +341,62 @@ impl Method { ret } - /// Translates a gRPC protobuf path to the corresponding generated Rust path. This is used to - /// translate the protobuf type definitions to their tonic generated Rust types. - /// - /// i.e. `.x.y.z` -> `crate::generated::x::y::z` + fn tonic_impl(&self) -> Function { + let mut ret = Function::new(to_snake_case(&self.name)); + ret.set_async(true) + .arg_ref_self() + .arg("request", format!("tonic::Request<{}>", self.request)) + .ret(format!("tonic::Result>", self.response)) + .line(format!("handle_unary::<{}>(request).await", self.name)); + + ret + } +} + +impl ServerStream { + fn from_descriptor(descriptor: &MethodDescriptorProto) -> Self { + let name = descriptor.name().to_string(); + + let request = grpc_path_to_generated(descriptor.input_type()); + let response = grpc_path_to_generated(descriptor.output_type()); + + Self { name, request, response } + } + + /// This stream's marker struct. /// - /// It also handles the case where the path is `.google.protobuf.Empty` by returning `()`. - fn grpc_path_to_generated(path: &str) -> String { - if path == ".google.protobuf.Empty" { - return "()".to_string(); - } + /// ```rust + /// pub struct ; + /// ``` + fn marker_struct(&self) -> Struct { + let mut ret = Struct::new(&self.name); + ret.vis("pub"); + ret + } + + fn tonic_impl(&self) -> Function { + let mut ret = Function::new(to_snake_case(&self.name)); + ret.set_async(true) + .arg_ref_self() + .arg("request", format!("tonic::Request<{}>", self.request)) + .ret(format!("tonic::Result>", self.associated_type().0)) + .line("todo!()"); + + ret + } - let path = path.trim_start_matches('.').replace('.', "::"); - format!("crate::generated::{path}") + fn associated_type(&self) -> (String, Type) { + ( + format!("{}Stream", self.name), + Type::new(format!( + "std::pin::Pin> + Send + 'static>>", + self.response + )), + ) } } -/// Converts a string to snake_case. +/// Converts a string to `snake_case`. fn to_snake_case(s: &str) -> String { let mut ret = String::new(); @@ -313,3 +411,18 @@ fn to_snake_case(s: &str) -> String { ret } + +/// Translates a gRPC protobuf path to the corresponding generated Rust path. This is used to +/// translate the protobuf type definitions to their tonic generated Rust types. +/// +/// i.e. `.x.y.z` -> `crate::generated::x::y::z` +/// +/// It also handles the case where the path is `.google.protobuf.Empty` by returning `()`. +fn grpc_path_to_generated(path: &str) -> String { + if path == ".google.protobuf.Empty" { + return "()".to_string(); + } + + let path = path.trim_start_matches('.').replace('.', "::"); + format!("crate::generated::{path}") +} diff --git a/crates/proto/src/server/mod.rs b/crates/proto/src/server/mod.rs index ce26c3c6ef..d9e70caf54 100644 --- a/crates/proto/src/server/mod.rs +++ b/crates/proto/src/server/mod.rs @@ -1,5 +1,3 @@ -pub mod remote_prover; - use core::fmt::Display; use futures::stream::Stream; @@ -51,19 +49,15 @@ pub trait GrpcServerStream: Send + Sync + 'static { /// Execute the standard unary flow: decode → handle → encode. /// /// Decode errors are mapped to `Status::invalid_argument`. -pub async fn handle_unary( - handler: &Handler, +pub async fn handle_unary( request: Request, ) -> Result, Status> where - Handler: GrpcUnary, - Handler::Input: GrpcDecode, - Handler::Output: GrpcEncode, Method: GrpcInterface, { - let input = Handler::Input::decode(request.into_inner()) + let input = Method::Input::decode(request.into_inner()) .map_err(|err| Status::invalid_argument(err.to_string()))?; - let output = handler.handle(input).await?; + let output = Method::handle(input).await?; let response = output.encode()?; Ok(Response::new(response)) } diff --git a/crates/proto/src/server/remote_prover.rs b/crates/proto/src/server/remote_prover.rs deleted file mode 100644 index c2e6b7c445..0000000000 --- a/crates/proto/src/server/remote_prover.rs +++ /dev/null @@ -1,74 +0,0 @@ -use std::task::{Context, Poll}; - -use tonic::{Request, Response, Status}; - -use crate::generated::remote_prover::{self as proto, api_server}; -use crate::server::{GrpcInterface, GrpcUnary, handle_unary}; - -pub struct ProveMethod; - -impl GrpcInterface for ProveMethod { - type Request = proto::ProofRequest; - type Response = proto::Proof; -} - -/// Public server trait for the remote prover API. -pub trait RemoteProverService: GrpcUnary {} - -impl RemoteProverService for T where T: GrpcUnary {} - -#[tonic::async_trait] -impl api_server::Api for T -where - T: RemoteProverService, -{ - async fn prove( - &self, - request: Request, - ) -> Result, Status> { - handle_unary::<_, ProveMethod>(self, request).await - } -} - -pub struct RemoteProverServer { - inner: api_server::ApiServer, -} - -impl RemoteProverServer -where - T: RemoteProverService, -{ - pub fn new(service: T) -> Self { - Self { - inner: api_server::ApiServer::new(service), - } - } -} - -impl Clone for RemoteProverServer { - fn clone(&self) -> Self { - Self { inner: self.inner.clone() } - } -} - -impl tonic::codegen::Service> for RemoteProverServer -where - api_server::ApiServer: tonic::codegen::Service>, -{ - type Response = - as tonic::codegen::Service>>::Response; - type Error = as tonic::codegen::Service>>::Error; - type Future = as tonic::codegen::Service>>::Future; - - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - self.inner.poll_ready(cx) - } - - fn call(&mut self, req: http::Request) -> Self::Future { - self.inner.call(req) - } -} - -impl tonic::server::NamedService for RemoteProverServer { - const NAME: &'static str = api_server::SERVICE_NAME; -} From 0b76ee030b20cb9887fd3cb9823f9a20bb76b2be Mon Sep 17 00:00:00 2001 From: Mirko von Leipzig <48352201+Mirko-von-Leipzig@users.noreply.github.com> Date: Mon, 2 Mar 2026 18:55:29 +0200 Subject: [PATCH 09/24] Compiles --- crates/proto/build.rs | 11 ++++++++--- crates/proto/src/server/mod.rs | 10 +++++++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/crates/proto/build.rs b/crates/proto/build.rs index 5776095e95..f1cd66bfa0 100644 --- a/crates/proto/build.rs +++ b/crates/proto/build.rs @@ -278,7 +278,12 @@ impl Service { } fn tonic_impl(&self) -> Impl { - let tonic_path = format!("crate::generated::{}::api_server::{}", self.package, self.name); + let tonic_path = format!( + "crate::generated::{}::{}_server::{}", + self.package, + to_snake_case(&self.name), + self.name + ); let mut ret = Impl::new("T"); ret.generic("T") @@ -347,7 +352,7 @@ impl UnaryMethod { .arg_ref_self() .arg("request", format!("tonic::Request<{}>", self.request)) .ret(format!("tonic::Result>", self.response)) - .line(format!("handle_unary::<{}>(request).await", self.name)); + .line(format!("handle_unary::<_, {}>(self, request).await", self.name)); ret } @@ -378,7 +383,7 @@ impl ServerStream { let mut ret = Function::new(to_snake_case(&self.name)); ret.set_async(true) .arg_ref_self() - .arg("request", format!("tonic::Request<{}>", self.request)) + .arg("_request", format!("tonic::Request<{}>", self.request)) .ret(format!("tonic::Result>", self.associated_type().0)) .line("todo!()"); diff --git a/crates/proto/src/server/mod.rs b/crates/proto/src/server/mod.rs index d9e70caf54..e9b9825a0e 100644 --- a/crates/proto/src/server/mod.rs +++ b/crates/proto/src/server/mod.rs @@ -49,15 +49,19 @@ pub trait GrpcServerStream: Send + Sync + 'static { /// Execute the standard unary flow: decode → handle → encode. /// /// Decode errors are mapped to `Status::invalid_argument`. -pub async fn handle_unary( +pub(crate) async fn handle_unary( + service: &Service, request: Request, ) -> Result, Status> where + Service: GrpcUnary, + Service::Input: GrpcDecode, + Service::Output: GrpcEncode, Method: GrpcInterface, { - let input = Method::Input::decode(request.into_inner()) + let input = Service::Input::decode(request.into_inner()) .map_err(|err| Status::invalid_argument(err.to_string()))?; - let output = Method::handle(input).await?; + let output = service.handle(input).await?; let response = output.encode()?; Ok(Response::new(response)) } From 84f64c0390960036e6a7242546aff395bfd934e6 Mon Sep 17 00:00:00 2001 From: Mirko von Leipzig <48352201+Mirko-von-Leipzig@users.noreply.github.com> Date: Tue, 3 Mar 2026 14:33:27 +0200 Subject: [PATCH 10/24] Simplify API to traits --- crates/proto/build.rs | 82 ++++++++++++++++++++++--------------------- 1 file changed, 42 insertions(+), 40 deletions(-) diff --git a/crates/proto/build.rs b/crates/proto/build.rs index f1cd66bfa0..f4f61690ea 100644 --- a/crates/proto/build.rs +++ b/crates/proto/build.rs @@ -211,17 +211,12 @@ impl Service { fn generate(&self) -> Module { let mut module = Module::new(&self.name); - module.import("crate::server", "GrpcInterface"); - module.import("crate::server", "GrpcUnary"); - module.import("crate::server", "handle_unary"); - module.push_trait(self.service_trait()); module.push_impl(self.blanket_impl()); module.push_impl(self.tonic_impl()); for method in &self.unary_methods { - module.push_struct(method.marker_struct()); - module.push_impl(method.grpc_interface_impl()); + module.push_trait(method.as_trait()); } for stream in &self.server_streams { @@ -248,7 +243,7 @@ impl Service { ret.vis("pub"); for method in &self.unary_methods { - ret.parent(method.unary_trait().ty()); + ret.parent(method.as_trait().ty()); } ret @@ -271,7 +266,7 @@ impl Service { ret.generic("T").impl_trait(self.service_trait().ty()); for method in &self.unary_methods { - ret.bound("T", method.unary_trait().ty()); + ret.bound("T", method.as_trait().ty()); } ret @@ -288,6 +283,9 @@ impl Service { let mut ret = Impl::new("T"); ret.generic("T") .bound("T", self.service_trait().ty()) + .bound("T", "Send") + .bound("T", "Sync") + .bound("T", "'static") .impl_trait(tonic_path) .r#macro("#[tonic::async_trait]"); @@ -314,45 +312,49 @@ impl UnaryMethod { Self { name, request, response } } - /// This [`Method`]'s marker struct. - /// - /// ```rust - /// pub struct ; - /// ``` - fn marker_struct(&self) -> Struct { - let mut ret = Struct::new(&self.name); - ret.vis("pub"); - ret - } + fn tonic_impl(&self) -> Function { + let mut ret = Function::new(to_snake_case(&self.name)); + ret.set_async(true) + .arg_ref_self() + .arg("request", format!("tonic::Request<{}>", self.request)) + .ret(format!("tonic::Result>", self.response)) + .line(format!( + "::full(&self, request.into_inner()).await.map(tonic::Response::new)", + self.name + )); - /// Returns this method's unary trait concrete type. - /// - /// ```rust - /// GrpcUnary - /// ``` - fn unary_trait(&self) -> Trait { - let mut ret = Trait::new("GrpcUnary"); - ret.generic(&self.name); ret } - /// This method's implementation of the `GrpcInterface` trait. - fn grpc_interface_impl(&self) -> Impl { - let mut ret = Impl::new(&self.name); - ret.impl_trait("GrpcInterface") - .associate_type("Request", &self.request) - .associate_type("Response", &self.response); + fn as_trait(&self) -> Trait { + let mut ret = Trait::new(&self.name); + ret.vis("pub"); + ret.attr("tonic::async_trait"); + ret.associated_type("Input"); + ret.associated_type("Output"); - ret - } + ret.new_fn("decode") + .arg("request", &self.request) + .ret("tonic::Result"); - fn tonic_impl(&self) -> Function { - let mut ret = Function::new(to_snake_case(&self.name)); - ret.set_async(true) + ret.new_fn("encode") + .arg("output", "Self::Output") + .ret(format!("tonic::Result<{}>", &self.response)); + + ret.new_fn("handle") + .set_async(true) .arg_ref_self() - .arg("request", format!("tonic::Request<{}>", self.request)) - .ret(format!("tonic::Result>", self.response)) - .line(format!("handle_unary::<_, {}>(self, request).await", self.name)); + .arg("input", "Self::Input") + .ret("tonic::Result"); + + ret.new_fn("full") + .set_async(true) + .arg_ref_self() + .arg("request", &self.request) + .ret(format!("tonic::Result<{}>", &self.response)) + .line("let input = Self::decode(request)?;") + .line("let output = self.handle(input).await?;") + .line("Self::encode(output)"); ret } From e70526d2e1f8a0e05c5d282ca78a983490842582 Mon Sep 17 00:00:00 2001 From: Mirko von Leipzig <48352201+Mirko-von-Leipzig@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:13:44 +0200 Subject: [PATCH 11/24] Use new simpler traits --- crates/proto/src/lib.rs | 1 - crates/proto/src/server/mod.rs | 67 ---------------------------------- 2 files changed, 68 deletions(-) delete mode 100644 crates/proto/src/server/mod.rs diff --git a/crates/proto/src/lib.rs b/crates/proto/src/lib.rs index 29e9e77b51..1ec05672cf 100644 --- a/crates/proto/src/lib.rs +++ b/crates/proto/src/lib.rs @@ -2,7 +2,6 @@ pub mod clients; pub mod decode; pub mod domain; pub mod errors; -pub mod server; #[rustfmt::skip] pub mod generated; diff --git a/crates/proto/src/server/mod.rs b/crates/proto/src/server/mod.rs deleted file mode 100644 index e9b9825a0e..0000000000 --- a/crates/proto/src/server/mod.rs +++ /dev/null @@ -1,67 +0,0 @@ -use core::fmt::Display; - -use futures::stream::Stream; -use tonic::{Request, Response, Status}; - -/// Decode a gRPC request body into a domain input type. -pub trait GrpcDecode: Sized + Send + Sync + 'static { - type Error: Display + Send + Sync + 'static; - - fn decode(input: T) -> Result; -} - -/// Encode a domain output into a gRPC response body. -/// -/// The encode consumes `self` so implementors can move out of the output. -pub trait GrpcEncode: Send + Sync + 'static { - fn encode(self) -> Result; -} - -pub trait GrpcInterface { - type Request; - type Response; -} - -/// Unary method handler. -/// -/// The method marker `M` is used to disambiguate multiple methods that share request/response -/// types. -#[tonic::async_trait] -pub trait GrpcUnary: Send + Sync + 'static { - type Input: GrpcDecode; - type Output: GrpcEncode; - - async fn handle(&self, input: Self::Input) -> Result; -} - -/// Server-streaming method handler. -/// -/// The method marker `M` is used to disambiguate multiple methods that share request/response -/// types. -#[tonic::async_trait] -pub trait GrpcServerStream: Send + Sync + 'static { - type Input: GrpcDecode; - type Stream: Stream> + Send + 'static; - - async fn handle(&self, input: Self::Input) -> Result; -} - -/// Execute the standard unary flow: decode → handle → encode. -/// -/// Decode errors are mapped to `Status::invalid_argument`. -pub(crate) async fn handle_unary( - service: &Service, - request: Request, -) -> Result, Status> -where - Service: GrpcUnary, - Service::Input: GrpcDecode, - Service::Output: GrpcEncode, - Method: GrpcInterface, -{ - let input = Service::Input::decode(request.into_inner()) - .map_err(|err| Status::invalid_argument(err.to_string()))?; - let output = service.handle(input).await?; - let response = output.encode()?; - Ok(Response::new(response)) -} From 2620d0cf2fb1cc1193ebfa3da79ca9c27e871ded Mon Sep 17 00:00:00 2001 From: Mirko von Leipzig <48352201+Mirko-von-Leipzig@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:18:13 +0200 Subject: [PATCH 12/24] Lints --- crates/proto/build.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/proto/build.rs b/crates/proto/build.rs index f4f61690ea..d05f2b448a 100644 --- a/crates/proto/build.rs +++ b/crates/proto/build.rs @@ -319,7 +319,7 @@ impl UnaryMethod { .arg("request", format!("tonic::Request<{}>", self.request)) .ret(format!("tonic::Result>", self.response)) .line(format!( - "::full(&self, request.into_inner()).await.map(tonic::Response::new)", + "::full(self, request.into_inner()).await.map(tonic::Response::new)", self.name )); From 8a2e66db7234795cf8bddb053bd395e347559f01 Mon Sep 17 00:00:00 2001 From: Mirko von Leipzig <48352201+Mirko-von-Leipzig@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:58:23 +0200 Subject: [PATCH 13/24] Improve build docs --- crates/proto/build.rs | 58 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/crates/proto/build.rs b/crates/proto/build.rs index d05f2b448a..f73a52f990 100644 --- a/crates/proto/build.rs +++ b/crates/proto/build.rs @@ -232,10 +232,10 @@ impl Service { /// /// ```rust /// trait : - /// GrpcUnary + - /// GrpcUnary + + /// method[0]::trait() + + /// method[1]::trait() + /// ... - /// GrpcUnary, + /// method[N]::trait(), /// {} /// ``` fn service_trait(&self) -> Trait { @@ -255,10 +255,10 @@ impl Service { /// ```rust /// impl for T /// where T: - /// GrpcUnary + - /// GrpcUnary + + /// method[0]::trait() + + /// method[1]::trait() + /// ... - /// GrpcUnary, + /// method[N]::trait(), /// {} /// ``` fn blanket_impl(&self) -> Impl { @@ -272,6 +272,22 @@ impl Service { ret } + /// Blanket implementation for all T that implement our service trait, for the tonic generated + /// trait. + /// + /// ```rust + /// #[tonic::async_trait] + /// impl tonic::generated::service_trait for T + /// where T: + /// + Send + Sync + 'static { + /// + /// async fn tonic_method[0](request) -> response { + /// ::full(self, request.into_inner()).await.map(tonic::Response::new) + /// } + /// + /// ... + /// } + /// ``` fn tonic_impl(&self) -> Impl { let tonic_path = format!( "crate::generated::{}::{}_server::{}", @@ -312,6 +328,15 @@ impl UnaryMethod { Self { name, request, response } } + /// Function invoking the method handler and mapping from/to tonic's request/response. + /// + /// ```rust + /// async fn ( + /// request: tonic::Request<>, + /// ) -> tonic::Result>> { + /// >::full(self, request.into_inner()).await.map(tonic::Response::new) + /// } + /// ``` fn tonic_impl(&self) -> Function { let mut ret = Function::new(to_snake_case(&self.name)); ret.set_async(true) @@ -326,6 +351,27 @@ impl UnaryMethod { ret } + /// This method's trait definition. + /// + /// ```rust + /// trait { + /// type Input; + /// type Output; + /// + /// fn decode(request: ) -> tonic::Result; + /// fn encode(output: Self::Output) -> tonic::Result; + /// async fn handle(&self, input: Self::Input) -> tonic::Result; + /// + /// async fn full( + /// &self, + /// request: , + /// ) -> tonic::Result<> { + /// let input = Self::decode(request)?; + /// let output = self.handle(input).await?; + /// Self::encode(output) + /// } + /// } + // /// ``` fn as_trait(&self) -> Trait { let mut ret = Trait::new(&self.name); ret.vis("pub"); From e4194b0608c6fc3ba31dcdbf9f9092a08ea23d81 Mon Sep 17 00:00:00 2001 From: Mirko von Leipzig <48352201+Mirko-von-Leipzig@users.noreply.github.com> Date: Tue, 3 Mar 2026 16:13:08 +0200 Subject: [PATCH 14/24] #[allow(clippy::unit_arg)] --- Cargo.lock | 1 - crates/proto/Cargo.toml | 1 - crates/proto/build.rs | 1 + 3 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8cd326cf77..92b6801c69 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3291,7 +3291,6 @@ dependencies = [ "build-rs", "codegen", "fs-err", - "futures", "hex", "http 1.4.0", "miden-node-grpc-error-macro", diff --git a/crates/proto/Cargo.toml b/crates/proto/Cargo.toml index 8a566ae485..42cb8aeb2e 100644 --- a/crates/proto/Cargo.toml +++ b/crates/proto/Cargo.toml @@ -16,7 +16,6 @@ workspace = true [dependencies] anyhow = { workspace = true } -futures = { workspace = true } hex = { version = "0.4" } http = { workspace = true } miden-node-grpc-error-macro = { workspace = true } diff --git a/crates/proto/build.rs b/crates/proto/build.rs index f73a52f990..7950a7f718 100644 --- a/crates/proto/build.rs +++ b/crates/proto/build.rs @@ -343,6 +343,7 @@ impl UnaryMethod { .arg_ref_self() .arg("request", format!("tonic::Request<{}>", self.request)) .ret(format!("tonic::Result>", self.response)) + .line("#[allow(clippy::unit_arg)]") .line(format!( "::full(self, request.into_inner()).await.map(tonic::Response::new)", self.name From 21ff0303ca56b68d29f6ac394b29b9a79cb0938b Mon Sep 17 00:00:00 2001 From: Krisztian Kovacs Date: Wed, 15 Apr 2026 18:06:54 +0200 Subject: [PATCH 15/24] fixup! Simplify --- crates/proto/build.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/proto/build.rs b/crates/proto/build.rs index 7950a7f718..08e19e3656 100644 --- a/crates/proto/build.rs +++ b/crates/proto/build.rs @@ -49,7 +49,6 @@ fn main() -> miette::Result<()> { .into_diagnostic() .wrap_err("generating server mod.rs")?; - // generate_server_modules(&descriptor_sets, &server_dst_dir)?; generate_mod_rs(&dst_dir).into_diagnostic().wrap_err("generating mod.rs")?; rustfmt_generated(&dst_dir)?; From 45e8b936f770803e44427f4b3c71bd973104bd22 Mon Sep 17 00:00:00 2001 From: Krisztian Kovacs Date: Fri, 17 Apr 2026 10:40:06 +0200 Subject: [PATCH 16/24] fix: skip formatting if rustfmt isn't installed --- crates/proto/build.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/crates/proto/build.rs b/crates/proto/build.rs index 08e19e3656..03857c95f7 100644 --- a/crates/proto/build.rs +++ b/crates/proto/build.rs @@ -1,5 +1,6 @@ +use std::io::ErrorKind; use std::path::Path; -use std::process::Command; +use std::process::{Command, ExitStatus}; use codegen::{Function, Impl, Module, Struct, Trait, Type}; use fs_err as fs; @@ -79,11 +80,14 @@ fn rustfmt_generated(dir: &Path) -> miette::Result<()> { return Ok(()); } - let status = Command::new("rustfmt") - .args(&rs_files) - .status() - .into_diagnostic() - .wrap_err("running rustfmt on generated files")?; + let status = match Command::new("rustfmt").args(&rs_files).status() { + Err(e) if e.kind() == ErrorKind::NotFound => { + // rustfmt is not installed, skip without an error + ExitStatus::default() + }, + Err(e) => return Err(e).into_diagnostic().wrap_err("running rustfmt on generated files"), + Ok(status) => status, + }; if !status.success() { miette::bail!("rustfmt failed with status: {status}"); From 24313e93a89df8f6af6fa29920f851ab41c84c06 Mon Sep 17 00:00:00 2001 From: Krisztian Kovacs Date: Fri, 17 Apr 2026 10:52:47 +0200 Subject: [PATCH 17/24] fix: generate (service, package) pairs only once Each FileDescriptorSet includes transitive imports, the same service (e.g. rpc.Api) will appear in multiple sets and the corresponding {module_name}.rs will be regenerated/overwritten multiple times. This commit fixes this. --- crates/proto/build.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/proto/build.rs b/crates/proto/build.rs index 03857c95f7..289176ac4d 100644 --- a/crates/proto/build.rs +++ b/crates/proto/build.rs @@ -1,3 +1,4 @@ +use std::collections::HashSet; use std::io::ErrorKind; use std::path::Path; use std::process::{Command, ExitStatus}; @@ -137,6 +138,8 @@ fn generate_server_modules( descriptor_sets: &[FileDescriptorSet], dst_dir: &Path, ) -> miette::Result<()> { + let mut generated: HashSet<(String, String)> = HashSet::new(); + for fds in descriptor_sets { for file in &fds.file { let package = file.package.as_deref().unwrap_or_default(); @@ -144,6 +147,11 @@ fn generate_server_modules( for service in &file.service { let service_name = service.name.as_deref().unwrap_or("Service"); + let key = (package.clone(), service_name.to_string()); + if !generated.insert(key) { + continue; + } + let service_name = to_snake_case(service_name); let module_name = format!("{}_{}", &package, service_name); From 052d18db71b70eba3624817c737450b1b2ad91e3 Mon Sep 17 00:00:00 2001 From: Krisztian Kovacs Date: Fri, 17 Apr 2026 10:55:56 +0200 Subject: [PATCH 18/24] fix: sort module names in mod.rs --- crates/proto/build.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/proto/build.rs b/crates/proto/build.rs index 289176ac4d..cf81de8f06 100644 --- a/crates/proto/build.rs +++ b/crates/proto/build.rs @@ -130,6 +130,7 @@ fn generate_mod_rs(dst_dir: impl AsRef) -> std::io::Result<()> { modules.push(format!("pub mod {module};")); } + modules.sort(); fs::write(dst_dir.as_ref().join("mod.rs"), modules.join("\n")) } From 706d7acc352f469810b426fb3cbc8cd87b8ad48d Mon Sep 17 00:00:00 2001 From: KOVACS Krisztian Date: Fri, 17 Apr 2026 11:52:51 +0200 Subject: [PATCH 19/24] Update crates/proto/build.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- crates/proto/build.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/proto/build.rs b/crates/proto/build.rs index cf81de8f06..80b333cd8d 100644 --- a/crates/proto/build.rs +++ b/crates/proto/build.rs @@ -446,7 +446,10 @@ impl ServerStream { .arg_ref_self() .arg("_request", format!("tonic::Request<{}>", self.request)) .ret(format!("tonic::Result>", self.associated_type().0)) - .line("todo!()"); + .line(format!( + "Err(tonic::Status::unimplemented({:?}))", + format!("server-streaming RPC `{}` is not implemented", self.name) + )); ret } From 82b834c4f6cf850e8d0ece17e1e4b88007677d03 Mon Sep 17 00:00:00 2001 From: Krisztian Kovacs Date: Fri, 17 Apr 2026 11:58:47 +0200 Subject: [PATCH 20/24] fix: return error instead of panicking on unsupported client streaming RPCs --- crates/proto/build.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/proto/build.rs b/crates/proto/build.rs index 80b333cd8d..cf4a41d0ef 100644 --- a/crates/proto/build.rs +++ b/crates/proto/build.rs @@ -157,7 +157,7 @@ fn generate_server_modules( let module_name = format!("{}_{}", &package, service_name); let contents = - Service::from_descriptor(service, &package).generate().scope().to_string(); + Service::from_descriptor(service, &package)?.generate().scope().to_string(); let path = dst_dir.join(format!("{module_name}.rs")); fs::write(path, contents).into_diagnostic().wrap_err("writing server module")?; @@ -188,7 +188,7 @@ struct ServerStream { } impl Service { - fn from_descriptor(descriptor: &ServiceDescriptorProto, package: &str) -> Self { + fn from_descriptor(descriptor: &ServiceDescriptorProto, package: &str) -> miette::Result { let name = descriptor.name().to_string(); let unary_methods = descriptor .method @@ -205,17 +205,17 @@ impl Service { let package = package.to_string(); // We don't have any client streams, so no need to support them. - assert!( + miette::ensure!( !descriptor.method.iter().any(MethodDescriptorProto::client_streaming), "client streams are not supported" ); - Self { + Ok(Self { name, package, unary_methods, server_streams, - } + }) } /// Generates a module containing the service's interface and implementation, including the From 4cf493ea40c039e5805807b5223dd93e54f7cfe6 Mon Sep 17 00:00:00 2001 From: KOVACS Krisztian Date: Fri, 17 Apr 2026 12:01:36 +0200 Subject: [PATCH 21/24] Update crates/proto/build.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- crates/proto/build.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/proto/build.rs b/crates/proto/build.rs index cf4a41d0ef..e6bab0ef28 100644 --- a/crates/proto/build.rs +++ b/crates/proto/build.rs @@ -258,6 +258,9 @@ impl Service { ret.parent(method.as_trait().ty()); } + for method in &self.server_streaming_methods { + ret.parent(method.as_trait().ty()); + } ret } From 1f27e821a5ac2d963d30681779554565a71b091d Mon Sep 17 00:00:00 2001 From: Krisztian Kovacs Date: Fri, 17 Apr 2026 12:04:42 +0200 Subject: [PATCH 22/24] Revert "Update crates/proto/build.rs" This reverts commit 4cf493ea40c039e5805807b5223dd93e54f7cfe6. --- crates/proto/build.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/crates/proto/build.rs b/crates/proto/build.rs index e6bab0ef28..cf4a41d0ef 100644 --- a/crates/proto/build.rs +++ b/crates/proto/build.rs @@ -258,9 +258,6 @@ impl Service { ret.parent(method.as_trait().ty()); } - for method in &self.server_streaming_methods { - ret.parent(method.as_trait().ty()); - } ret } From 6149e1c383bea7061a43fdd285cb2939e4f05d6e Mon Sep 17 00:00:00 2001 From: Krisztian Kovacs Date: Fri, 17 Apr 2026 14:06:55 +0200 Subject: [PATCH 23/24] fix: handle server streaming methods --- crates/proto/build.rs | 80 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 71 insertions(+), 9 deletions(-) diff --git a/crates/proto/build.rs b/crates/proto/build.rs index cf4a41d0ef..3c4f1c360a 100644 --- a/crates/proto/build.rs +++ b/crates/proto/build.rs @@ -3,7 +3,7 @@ use std::io::ErrorKind; use std::path::Path; use std::process::{Command, ExitStatus}; -use codegen::{Function, Impl, Module, Struct, Trait, Type}; +use codegen::{Function, Impl, Module, Trait, Type}; use fs_err as fs; use miden_node_proto_build::{ block_producer_api_descriptor, @@ -232,7 +232,7 @@ impl Service { } for stream in &self.server_streams { - module.push_struct(stream.marker_struct()); + module.push_trait(stream.as_trait()); } module @@ -258,6 +258,10 @@ impl Service { ret.parent(method.as_trait().ty()); } + for stream in &self.server_streams { + ret.parent(stream.as_trait().ty()); + } + ret } @@ -281,6 +285,10 @@ impl Service { ret.bound("T", method.as_trait().ty()); } + for stream in &self.server_streams { + ret.bound("T", stream.as_trait().ty()); + } + ret } @@ -429,14 +437,68 @@ impl ServerStream { Self { name, request, response } } - /// This stream's marker struct. + /// This stream's per-method trait definition. /// /// ```rust - /// pub struct ; + /// trait { + /// type Input; + /// type Item; + /// type ItemStream: Stream> + Send + 'static; + /// + /// fn decode(request: ) -> tonic::Result; + /// fn encode(item: Self::Item) -> tonic::Result; + /// async fn handle(&self, input: Self::Input) -> tonic::Result; + /// + /// async fn full(&self, request: ) -> tonic::Result>>> { + /// use tokio_stream::StreamExt as _; + /// let input = Self::decode(request)?; + /// let stream = self.handle(input).await?; + /// Ok(Box::pin(stream.map(|item| item.and_then(|i| Self::encode(i))))) + /// } + /// } /// ``` - fn marker_struct(&self) -> Struct { - let mut ret = Struct::new(&self.name); + fn as_trait(&self) -> Trait { + let stream_bound = + format!("tonic::codegen::tokio_stream::Stream>"); + let boxed_stream = format!( + "std::pin::Pin> + Send + 'static>>", + self.response + ); + + let mut ret = Trait::new(&self.name); ret.vis("pub"); + ret.attr("tonic::async_trait"); + ret.associated_type("Input"); + ret.associated_type("Item"); + ret.associated_type("ItemStream") + .bound(&stream_bound) + .bound("Send") + .bound("'static"); + + ret.new_fn("decode") + .arg("request", &self.request) + .ret("tonic::Result"); + + ret.new_fn("encode") + .arg("item", "Self::Item") + .ret(format!("tonic::Result<{}>", &self.response)); + + ret.new_fn("handle") + .set_async(true) + .arg_ref_self() + .arg("input", "Self::Input") + .ret("tonic::Result"); + + ret.new_fn("full") + .set_async(true) + .arg_ref_self() + .arg("request", &self.request) + .ret(format!("tonic::Result<{boxed_stream}>")) + .line("use tonic::codegen::tokio_stream::StreamExt as _;") + .line("let input = Self::decode(request)?;") + .line("let stream = self.handle(input).await?;") + .line("Ok(Box::pin(stream.map(|item| item.and_then(|i| Self::encode(i)))))"); + ret } @@ -444,11 +506,11 @@ impl ServerStream { let mut ret = Function::new(to_snake_case(&self.name)); ret.set_async(true) .arg_ref_self() - .arg("_request", format!("tonic::Request<{}>", self.request)) + .arg("request", format!("tonic::Request<{}>", self.request)) .ret(format!("tonic::Result>", self.associated_type().0)) .line(format!( - "Err(tonic::Status::unimplemented({:?}))", - format!("server-streaming RPC `{}` is not implemented", self.name) + "::full(self, request.into_inner()).await.map(tonic::Response::new)", + self.name )); ret From 3c92ff89ea1938a9f0d5a74e7d1dbda7c975a787 Mon Sep 17 00:00:00 2001 From: Krisztian Kovacs Date: Fri, 17 Apr 2026 14:56:55 +0200 Subject: [PATCH 24/24] chore: clippy fixes --- crates/proto/build.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/proto/build.rs b/crates/proto/build.rs index 3c4f1c360a..8e23179ac4 100644 --- a/crates/proto/build.rs +++ b/crates/proto/build.rs @@ -459,7 +459,7 @@ impl ServerStream { /// ``` fn as_trait(&self) -> Trait { let stream_bound = - format!("tonic::codegen::tokio_stream::Stream>"); + "tonic::codegen::tokio_stream::Stream>".to_string(); let boxed_stream = format!( "std::pin::Pin> + Send + 'static>>", self.response @@ -508,6 +508,7 @@ impl ServerStream { .arg_ref_self() .arg("request", format!("tonic::Request<{}>", self.request)) .ret(format!("tonic::Result>", self.associated_type().0)) + .line("#[allow(clippy::unit_arg)]") .line(format!( "::full(self, request.into_inner()).await.map(tonic::Response::new)", self.name