Skip to content

libain gRPC intgration

Ravi Shankar edited this page May 11, 2022 · 12 revisions

Background

libain-rs takes advantage of Rust async/await to offer JSON RPC and gRPC servers. It also generates the necessary C++ glue code for seamlessly transitioning RPC functions to FFI functions which are then used by the library to serve RPC calls.

Advantages:

  • Having a typed language-agnostic spec (protobuf) which can then be used by other languages to generate their own clients and communicate through gRPC
  • Getting rid of libevent dependency in C++
  • Pave way for a new testing framework in Rust

Most of the work is automated in the Rust side (through codegen generated from build script), so you'll only have to modify the protobuf files and add/change C++ functions. This should automatically reflect in the served RPCs.

Exposed functions from Rust

  • init_runtime: To instantiate logger and runtime (event loop, scheduler and executor). The ownership is then given to C++ through an opaque type, which is then required for the rest of the functions.
  • start_servers: Spawns RPC servers in a separate thread bound to the provided addresses.
  • stop_servers: Consumes the runtime once and for all and shuts down the servers.

Adding new RPC call

Let's take getbestblockhash function as an example.

Firstly, we should write a fair enough protobuf definition for this function:

syntax = "proto3";
package rpc;

import "google/protobuf/empty.proto";
import "types/block.proto";

service Blockchain {
    // Returns the hash of the best (tip) block in the most-work fully-validated chain.
    rpc GetBestBlockHash(google.protobuf.Empty) returns (types.BlockResult);
}

This will reside in protobuf/rpc/blockchain.proto inside libain-rs

The corresponding type will be in protobuf/types/block.proto

syntax = "proto3";
package types;

message BlockResult {
    string hash = 1; // Hex-encoded data for block hash
}

In codegen.rs, we already have:

pub mod types {
    tonic::include_proto!("types");
}

pub mod rpc {
    tonic::include_proto!("rpc");
}

This will include the code generated from both rpc and types protobuf modules.

As we've added a new service, we have to mount it on both JSON RPC and gRPC servers. This can be done in lib.rs. All we have to do is import BlockchainService from codegen::rpc and invoke BlockchainService::service() and BlockchainService::module() functions for adding them to the corresponding servers.

During compilation, Rust will emit the C++ function signature inside libain.cpp and libain.hpp files in target/ directory. The generated Rust function will look like:

fn GetBestBlockHash(result: &mut BlockResult) -> Result<()>;

You can debug the generated code by navigating to target/debug/build/ain-grpc-{checksum}/out/*.rs and run cargo fmt -- target/debug/build/ain-grpc-*/out/*.rs to format the code for readability.

The corresponding C++ function will look like:

void GetBestBlockHash(BlockResult &result);

Note that the return value is passed as a mutable reference. This way, Rust will have exclusive ownership to the struct and we don't have to move/free anything in C++ side.

These changes should then be pushed to a branch in libain-rs repo.

Adding C++ function

libain-rs branch should now be reflected in depends/packages/libain.mk inside ain repo.

Based on the emitted function signature, the actual implementation in defid will now look like:

void GetBestBlockHash(BlockResult &result)
{
    LOCK(cs_main);
    result.hex_data = ::ChainActive().Tip()->GetBlockHash().GetHex();
}

Now, if we run ./make.sh build, we'll have the newly added function as part of defid.