Skip to content

feat(proto): generate generic GRPC API implementations#1950

Open
kkovaacs wants to merge 15 commits intonextfrom
krisztian/grpc-servers
Open

feat(proto): generate generic GRPC API implementations#1950
kkovaacs wants to merge 15 commits intonextfrom
krisztian/grpc-servers

Conversation

@kkovaacs
Copy link
Copy Markdown
Contributor

This PR adds the build-time generation of generic GRPC API implementations for all services discussed in PR #1742.

For each method we're generating a trait like this:

#[tonic::async_trait]
pub trait Status {
    type Input;
    type Output;

    fn decode(request: ()) -> tonic::Result<Self::Input>;

    fn encode(output: Self::Output) -> tonic::Result<crate::generated::validator::ValidatorStatus>;

    async fn handle(&self, input: Self::Input) -> tonic::Result<Self::Output>;

    async fn full(
        &self,
        request: (),
    ) -> tonic::Result<crate::generated::validator::ValidatorStatus> {
        let input = Self::decode(request)?;
        let output = self.handle(input).await?;
        Self::encode(output)
    }
}

The idea is that the actual implementation should consist of implementing all per-method traits for the API server (most of the time just decode / encode / handle), and then the provided generic implementation for the API can be used:

pub trait ApiService: Status + SubmitProvenTransaction + SignBlock {}

impl<T> ApiService for T
where
    T: Status,
    T: SubmitProvenTransaction,
    T: SignBlock,
{
}

#[tonic::async_trait]
impl<T> crate::generated::validator::api_server::Api for T
where
    T: ApiService,
    T: Send,
    T: Sync,
    T: 'static,
{
    async fn status(
        &self,
        request: tonic::Request<()>,
    ) -> tonic::Result<tonic::Response<crate::generated::validator::ValidatorStatus>> {
        #[allow(clippy::unit_arg)]
        <T as Status>::full(self, request.into_inner()).await.map(tonic::Response::new)
    }

    async fn submit_proven_transaction(
        &self,
        request: tonic::Request<crate::generated::transaction::ProvenTransaction>,
    ) -> tonic::Result<tonic::Response<()>> {
        #[allow(clippy::unit_arg)]
        <T as SubmitProvenTransaction>::full(self, request.into_inner())
            .await
            .map(tonic::Response::new)
    }

    async fn sign_block(
        &self,
        request: tonic::Request<crate::generated::blockchain::ProposedBlock>,
    ) -> tonic::Result<tonic::Response<crate::generated::blockchain::BlockSignature>> {
        #[allow(clippy::unit_arg)]
        <T as SignBlock>::full(self, request.into_inner())
            .await
            .map(tonic::Response::new)
    }
}

In this form this PR should be a no-op, because the none of our API implementations have been moved to this new model. That will be done in multiple follow-up PRs.

@kkovaacs kkovaacs added the no changelog This PR does not require an entry in the `CHANGELOG.md` file label Apr 16, 2026
@kkovaacs kkovaacs marked this pull request as ready for review April 16, 2026 11:29
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the crates/proto build-time code generation to also emit generic, per-RPC-method server-side traits and blanket tonic server implementations, aiming to standardize gRPC server implementations across all services referenced in PR #1742.

Changes:

  • Update crates/proto/build.rs to generate additional server facade modules from protobuf descriptors (plus regenerate mod.rs files).
  • Add build-time dependencies needed for code generation and descriptor introspection (codegen, prost-types).
  • Run rustfmt over generated Rust sources at build time.

Reviewed changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated 4 comments.

File Description
crates/proto/build.rs Adds server module generation from descriptors, new mod.rs generation logic, and build-time rustfmt pass.
crates/proto/Cargo.toml Adds build dependencies (codegen, prost-types) needed by build.rs.
Cargo.toml Pins prost-types to align with pinned prost versions in the workspace.
Cargo.lock Locks new dependency resolutions for codegen and prost-types.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/proto/build.rs
Comment thread crates/proto/build.rs
Comment on lines +74 to +90
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}");
}
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rustfmt_generated unconditionally invokes the rustfmt binary during the build script. This adds a non-trivial build-time cost and makes builds depend on an external tool being available/working for the host toolchain. Consider gating formatting behind an env var (e.g. only in CI or when explicitly enabled), or gracefully skipping formatting when rustfmt isn't found, so consumers can still build the crate.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kkovaacs This is maybe a good idea (skipping if not found).

Comment thread crates/proto/build.rs
Comment on lines +136 to +153
for fds in descriptor_sets {
for file in &fds.file {
let package = file.package.as_deref().unwrap_or_default();
let package = package.replace('.', "_");

for service in &file.service {
let service_name = service.name.as_deref().unwrap_or("Service");
let service_name = to_snake_case(service_name);
let module_name = format!("{}_{}", &package, service_name);

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")?;
}
}
}
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generate_server_modules iterates every FileDescriptorSet and every file within each set. Because each descriptor set 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. Consider tracking generated (package, service) pairs in a HashSet and skipping duplicates to reduce build time and avoid accidental divergence if descriptor compilation ever differs.

Copilot uses AI. Check for mistakes.
Comment thread crates/proto/build.rs

modules.push(format!("pub mod {module};"));
}

Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generate_mod_rs builds the module list from read_dir without sorting. Directory iteration order is filesystem-dependent, so the generated mod.rs can change nondeterministically across machines/runs, causing unnecessary rebuilds and flaky diffs in generated output. Collect module names, sort them, then write them in a stable order.

Suggested change
modules.sort();

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kkovaacs This should be taken care of by rustfmt step, but in the case this isn't found, maybe clippy will still complain, making this worth doing?

Base automatically changed from krisztian/proto-use-codegen-for-descriptor-generation to next April 16, 2026 19:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no changelog This PR does not require an entry in the `CHANGELOG.md` file

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants