Serialization in Exonum

Exonum uses Protocol Buffers (aka Protobuf) as its serialization format for communication among full nodes, cryptographic operations on light clients and storage of data. Protobuf is the industry accepted language-neutral and platform-neutral automated mechanism for serializing data.

Usage

Communication Among Full Nodes

Full nodes can both serialize messages for sending and deserialize messages when they are received. All the information that passes in the network between nodes turns into messages.

Communication with Light Clients

Light clients form messages which include transactions serialized in protobuf, sign them and send to the network.

Storage of Data

The storage is used to place blocks, configurations, data specific for services. Data obtained from the storage is not validated, since it is assumed to be validated earlier.

Using Protobuf from Rust

To apply protobuf serialization to structures in Exonum, users are required to describe the structures in a file with .proto extension, using the protobuf interface description language. All the .proto files are then combined into a single module which is used to generate files with .rs extension. The .rs files handle the serialization of structures described in them. And that is it, the described structures can then be serialized and deserialized in Exonum.

For example, a simple Timestamp structure will have following description in a .proto file:

message Timestamp {
  int64 seconds = 1;
  int32 nanos = 2;
}

The same structure will have the following representation in an .rs protobuf-generated file:

pub struct Timestamp {
    // message fields
    pub seconds: i64,
    pub nanos: i32,
    // special fields skipped...
}

Besides the description of structures, the .rs files also contain additional code and functions required for protobuf serialization and deserialization.

For convenience, the protobuf descriptions of the typical structures used in Exonum are already included in the framework.

Building Exonum with Protobuf Serialization

Exonum includes the exonum-build crate which lets users add the protobuf_generate function to their build.rs. This function automatically generates the .rs files for all the .proto files during the build process. exonum-build needs to be added to the project as a build dependency. To use protobuf_generate, add the following code to your build script (build.rs) indicating the folder which contains the .proto files:

use exonum_build::{ProtoSources, ProtobufGenerator};

fn main() {
    ProtobufGenerator::with_mod_name("protobuf_mod.rs")
        .with_input_dir("src/proto")
        // The exact list of included files may differ depending on
        // what Protobuf messages defined in Exonum you need to use.
        .with_includes(&[
            "src/proto".into(),
            ProtoSources::Exonum,
            ProtoSources::Crypto,
        ])
        .generate();
}

To use Protobuf-generated Rust structures, you first need to create a module which will include the Protobuf-generated files:

include!(concat!(env!("OUT_DIR"), "/example_mod.rs"));

// If you use types from `exonum` .proto files.
use exonum::proto::schema::*;

For example, the generated Wallet structure from the cryptocurrency.proto file, which resides in the proto module, will be available using proto::cryptocurrency::Wallet.

Tip

An example of this workflow can be found in the cryptocurrency example service.

Additional Validation for Protobuf-Generated Structures

Protobuf is a versatile and flexible tool, which presents not only opportunities but also certain complications for the Exonum framework. For example, fields in protobuf cannot be fixed-size arrays, however, fixed-size arrays are required in Exonum (e.g. for hashes). It is possible to implement additional validations using the .rs protobuf-generated files. However, if users work with protobuf-generated structures, field validation would need to be performed every time they are used.

To have validation performed only once for the whole structure, Exonum provides the conversion mechanism using the ProtobufConvert trait. This trait lets users automatically map their structures and the structures generated from .proto descriptions, providing a mechanism for validating protobuf-generated data. The structures for ProtobufConvert should have the same fields as the structures in .proto files, but can contain additional validation.

The exonum-derive crate provides the ability to use structures typical for Exonum with all the required validations. So when using these structures users only need to implement #[derive(ProtobufConvert)] for them. If required, users can implement the ProtobufConvert trait for any other structure they need to add that cannot be described using Protobuf IDL.

For example, the protobuf description of the SignedMessage in Exonum is as follows:

message SignedMessage {
  bytes payload = 1;
  exonum.crypto.PublicKey author = 2;
  exonum.crypto.Signature signature = 3;
}

The corresponding SignedMessage structure with ProtobufConvert has the following representation:

use crate::proto::schema::messages;

#[derive(Clone, PartialEq, Eq, Ord, PartialOrd, Debug)]
#[derive(ProtobufConvert, BinaryValue, ObjectHash)]
#[protobuf_convert(source = "messages::SignedMessage")]
pub struct SignedMessage {
    pub payload: Vec<u8>,
    pub author: PublicKey,
    pub signature: Signature,
}

Note that it is required to indicate the protobuf structure to which the current structure refers, in the case above messages::SignedMessage.