diff --git a/abci/Cargo.toml b/abci/Cargo.toml index bdd4c50..ce90065 100644 --- a/abci/Cargo.toml +++ b/abci/Cargo.toml @@ -1,5 +1,5 @@ [package] -version = "0.14.0-dev.6" +version = "0.14.0-dev.7" name = "tenderdash-abci" edition = "2021" license = "Apache-2.0" @@ -11,7 +11,15 @@ description = """tenderdash-abci provides a simple framework with which to build low-level applications on top of Tenderdash.""" [features] -default = ["server", "docker-tests", "crypto", "tcp", "unix", "tracing-span"] +default = [ + "server", + "docker-tests", + "crypto", + "tcp", + "unix", + "grpc", + "tracing-span", +] # docker-tests includes integration tests that require docker to be available docker-tests = ["server"] server = [ @@ -20,6 +28,8 @@ server = [ "dep:tokio-util", "dep:futures", ] + +grpc = ["tenderdash-proto/grpc"] crypto = ["dep:lhash"] tcp = ["server"] unix = ["server"] @@ -66,3 +76,5 @@ futures = { version = "0.3.26" } tokio = { version = "1", features = ["macros", "signal", "time", "io-std"] } hex = { version = "0.4" } lazy_static = { version = "1.4" } +# For tests of gRPC server +tonic = { version = "0.11" } diff --git a/abci/src/lib.rs b/abci/src/lib.rs index 53e983d..fc86059 100644 --- a/abci/src/lib.rs +++ b/abci/src/lib.rs @@ -22,7 +22,7 @@ pub use application::{check_version, Application, RequestDispatcher}; use prost::{DecodeError, EncodeError}; #[allow(deprecated)] #[cfg(feature = "server")] -pub use server::{start_server, CancellationToken, Server, ServerBuilder}; +pub use server::{start_server, CancellationToken, Server, ServerBuilder, ServerRuntime}; pub use tenderdash_proto as proto; #[cfg(feature = "crypto")] diff --git a/abci/src/server.rs b/abci/src/server.rs index 630379a..f9cd02f 100644 --- a/abci/src/server.rs +++ b/abci/src/server.rs @@ -8,11 +8,15 @@ use std::{ str::FromStr, }; +use futures::Future; #[cfg(feature = "tcp")] use tokio::net::TcpListener; #[cfg(feature = "unix")] use tokio::net::UnixListener; -use tokio::runtime::{Handle, Runtime}; +use tokio::{ + runtime::{Handle, Runtime}, + task::JoinHandle, +}; pub use tokio_util::sync::CancellationToken; use self::generic::GenericServer; @@ -208,10 +212,24 @@ impl<'a, App: RequestDispatcher + 'a> ServerBuilder { } /// Server runtime that must be alive for the whole lifespan of the server -pub(crate) struct ServerRuntime { +pub struct ServerRuntime { /// Runtime stored here to ensure it is never dropped _runtime: Option, - handle: Handle, + pub handle: Handle, +} + +impl ServerRuntime { + pub fn block_on(&self, future: F) -> F::Output { + self.handle.block_on(future) + } + + pub fn spawn(&self, future: F) -> JoinHandle + where + F: Future + Send + 'static, + F::Output: Send + 'static, + { + self.handle.spawn(future) + } } impl Default for ServerRuntime { diff --git a/abci/tests/common/docker.rs b/abci/tests/common/docker.rs index 9d084ca..1645dc4 100644 --- a/abci/tests/common/docker.rs +++ b/abci/tests/common/docker.rs @@ -6,7 +6,8 @@ use bollard::{ Docker, API_DEFAULT_VERSION, }; use futures::StreamExt; -use tokio::{io::AsyncWriteExt, runtime::Runtime, time::timeout}; +use tenderdash_abci::ServerRuntime; +use tokio::{io::AsyncWriteExt, time::timeout}; use tracing::{debug, error, info}; use url::Url; @@ -16,7 +17,7 @@ pub struct TenderdashDocker { name: String, docker: Docker, image: String, - runtime: Runtime, + runtime: ServerRuntime, } impl TenderdashDocker { /// new() creates and starts new Tenderdash docker container for provided @@ -31,8 +32,8 @@ impl TenderdashDocker { /// /// * `tag` - Docker tag to use; provide empty string to use default /// * `app_address` - address of ABCI app server; for example, - /// `tcp://172.17.0.1:4567`, `tcp://[::ffff:ac11:1]:5678` or - /// `unix:///path/to/file` + /// `tcp://172.17.0.1:4567`, `tcp://[::ffff:ac11:1]:5678`, + /// `grpc://172.17.01:5678` or `unix:///path/to/file` pub(crate) fn new( container_name: &str, tag: Option<&str>, @@ -45,14 +46,14 @@ impl TenderdashDocker { }; let app_address = url::Url::parse(app_address).expect("invalid app address"); - if app_address.scheme() != "tcp" && app_address.scheme() != "unix" { - panic!("app_address must be either tcp:// or unix://"); + if app_address.scheme() != "tcp" + && app_address.scheme() != "unix" + && app_address.scheme() != "grpc" + { + panic!("app_address must be either grpc://, tcp:// or unix://"); } - let runtime = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .expect("cannot initialize tokio runtime"); + let runtime = tenderdash_abci::ServerRuntime::default(); info!("Starting Tenderdash docker container"); @@ -152,12 +153,24 @@ impl TenderdashDocker { None }; - let app_address = app_address.to_string().replace('/', "\\/"); + let (abci, app_address) = match app_address.scheme() { + "grpc" => { + let address = app_address + .to_string() + .replace("grpc://", "") + .replace('/', "\\/"); + ("grpc", address) + }, + _ => ("socket", app_address.to_string().replace('/', "\\/")), + }; debug!("Tenderdash will connect to ABCI address: {}", app_address); let container_config = Config { image: Some(self.image.clone()), - env: Some(vec![format!("PROXY_APP={}", app_address)]), + env: Some(vec![ + format!("PROXY_APP={}", app_address), + format!("ABCI={}", abci), + ]), host_config: Some(HostConfig { binds, ..Default::default() @@ -263,6 +276,7 @@ impl Drop for TenderdashDocker { } } } + /// Use custom panic handler to dump logs on panic #[allow(dead_code)] pub fn setup_td_logs_panic(td_docker: &Arc) { diff --git a/abci/tests/grpc.rs b/abci/tests/grpc.rs new file mode 100644 index 0000000..b75516c --- /dev/null +++ b/abci/tests/grpc.rs @@ -0,0 +1,148 @@ +//! Test gRPC server for ABCI protocol. +//! +//! This test verifies that the gRPC server generated with tonic as part of the +//! tenderdash-proto crate can successfully connect to Tenderdash instance. +//! +//! This test should be implemented in the tenderdash-proto crate; however, it +//! is implemented here to use already existing docker container testing +//! logic. +#![cfg(feature = "grpc")] + +use std::sync::Arc; + +use tenderdash_abci::{ + proto::abci::{ + abci_application_server::AbciApplication, RequestEcho, RequestInfo, ResponseInfo, + }, + CancellationToken, +}; +mod common; +use tenderdash_abci::proto; +use tonic::{async_trait, Response, Status}; + +#[cfg(feature = "docker-tests")] +#[tokio::test] +/// Test server listening on ipv4 address. +/// +/// See [tcp_server_test()]. +async fn test_ipv4_server() { + // we assume the host uses default Docker network configuration, with the host + // using 172.17.0.1 + let bind_address = "172.17.0.1:1234".to_string(); + + grpc_server_test("v4", bind_address.as_str()).await; +} + +#[cfg(feature = "docker-tests")] +#[tokio::test] +/// Test server listening on ipv6 address. +/// +/// See [tcp_server_test()]. +async fn test_ipv6_server() { + // we assume the host uses default Docker network configuration, with the host + // using 172.17.0.1. This is IPv6 notation of the IPv4 address. + let bind_address = "[::ffff:ac11:1]:5678".to_string(); + + grpc_server_test("v6", bind_address.as_str()).await; +} + +#[cfg(feature = "docker-tests")] +/// Feature: ABCI App TCO server +/// +/// * Given that we have Tenderdash instance using TCP connection to communicate +/// with ABCI APP +/// * When we estabilish connection with Tenderdash +/// * Then Tenderdash sends Info request +async fn grpc_server_test(test_name: &str, bind_address: &str) { + use core::panic; + + use proto::abci::abci_application_server::AbciApplicationServer; + use tonic::transport::Server; + + tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::new("debug")) + .with_ansi(true) + .try_init() + .ok(); + + let cancel = CancellationToken::new(); + let app = TestApp { + cancel: cancel.clone(), + }; + + let addr = bind_address.parse().expect("address must be valid"); + let server_cancel = cancel.clone(); + let server_handle = tokio::spawn(async move { + tracing::debug!("starting gRPC server"); + Server::builder() + .add_service(AbciApplicationServer::new(app)) + .serve_with_shutdown(addr, server_cancel.cancelled()) + .await + .expect("server failed"); + tracing::debug!("gRPC server stopped"); + }); + + let socket_uri = format!("grpc://{}", bind_address); + let container_name = format!("tenderdash_{}", test_name); + + let td = tokio::task::spawn_blocking(move || { + tracing::debug!("starting Tenderdash in Docker container"); + let td = Arc::new(common::docker::TenderdashDocker::new( + &container_name, + None, + &socket_uri, + )); + common::docker::setup_td_logs_panic(&td); + tracing::debug!("started Tenderdash in Docker container"); + + td + }) + .await + .expect("start tenderdash"); + + tokio::select! { + _ = tokio::time::sleep(tokio::time::Duration::from_secs(60)) => { + panic!("Test timed out"); + } + _ = cancel.cancelled() => { + tracing::debug!("CancellationToken cancelled"); + } + ret = server_handle => { + ret.expect("gRPC server failed"); + } + } + + tokio::task::spawn_blocking(move || drop(td)) + .await + .expect("tenderdash cleanup"); + + tracing::info!("Test finished successfully"); +} + +pub struct TestApp { + // when test succeeds, we cancel this token to finish it + cancel: CancellationToken, +} +#[async_trait] +impl AbciApplication for TestApp { + async fn echo( + &self, + request: tonic::Request, + ) -> Result, Status> { + tracing::info!(?request, "Echo called"); + Ok(Response::new(proto::abci::ResponseEcho { + message: request.into_inner().message, + })) + } + async fn info( + &self, + _request: tonic::Request, + ) -> std::result::Result, tonic::Status> { + tracing::info!("Info called, test successful"); + let resp = ResponseInfo { + ..Default::default() + }; + self.cancel.cancel(); + Ok(Response::new(resp)) + } +} diff --git a/proto-compiler/Cargo.toml b/proto-compiler/Cargo.toml index 9980ade..2033d63 100644 --- a/proto-compiler/Cargo.toml +++ b/proto-compiler/Cargo.toml @@ -1,5 +1,5 @@ [package] -version = "0.14.0-dev.6" +version = "0.14.0-dev.7" name = "tenderdash-proto-compiler" authors = ["Informal Systems ", "Dash Core Group"] edition = "2021" @@ -17,3 +17,15 @@ regex = { "version" = "1.7.1" } ureq = { "version" = "2.6.2" } zip = { version = "0.6.4", default-features = false, features = ["deflate"] } fs_extra = { version = "1.3.0" } +tonic-build = { version = "0.11.0", optional = true } + + +[features] +default = [] +# Enable gRPC support; needed by server and client features. +# Conflicts with no_std +grpc = ["dep:tonic-build"] +# Build the gRPC server. Requires tenderdash-proto/std feature. +server = ["grpc"] +# Build the gRPC client. Requires tenderdash-proto/std feature. +client = ["grpc"] diff --git a/proto-compiler/src/lib.rs b/proto-compiler/src/lib.rs index 27f5e58..c308da2 100644 --- a/proto-compiler/src/lib.rs +++ b/proto-compiler/src/lib.rs @@ -107,6 +107,14 @@ pub fn proto_compile() { let tenderdash_ver = tenderdash_version(tenderdash_dir); println!("[info] => Creating structs."); + + #[cfg(feature = "grpc")] + tonic_build::configure() + .generate_default_stubs(true) + .compile_with_config(pb, &protos, &proto_includes_paths) + .unwrap(); + + #[cfg(not(feature = "grpc"))] pb.compile_protos(&protos, &proto_includes_paths).unwrap(); println!("[info] => Removing old structs and copying new structs."); diff --git a/proto/Cargo.toml b/proto/Cargo.toml index 3a7f060..b5e1fcd 100644 --- a/proto/Cargo.toml +++ b/proto/Cargo.toml @@ -1,5 +1,5 @@ [package] -version = "0.14.0-dev.6" +version = "0.14.0-dev.7" name = "tenderdash-proto" edition = "2021" license = "Apache-2.0" @@ -15,10 +15,33 @@ description = """ [package.metadata.docs.rs] all-features = true +[features] +# Features configuration. +# +# Note that, due to the way build.rs scripts work, change of features does not trigger +# regeneration of protobuf files. This means you need to be extra careful when changing +# features, as you might end up with outdated and/or conflicting generated files. +# +# Sometimes cleaning the build cache with `cargo clean` might be necessary to solve +# issues related to outdated generated files. +default = ["grpc"] + +# Enable standard library support +std = ["prost/std", "prost-types/std"] +# Build gRPC server +grpc = [ + "std", + "tenderdash-proto-compiler/server", + "tenderdash-proto-compiler/client", + "dep:tonic", +] + [dependencies] prost = { version = "0.12", default-features = false, features = [ "prost-derive", ] } +prost-types = { version = "0.12", default-features = false } +tonic = { version = "0.11", optional = true } bytes = { version = "1.0", default-features = false, features = ["serde"] } serde = { version = "1.0", default-features = false, features = ["derive"] } subtle-encoding = { version = "0.5", default-features = false, features = [ diff --git a/proto/build.rs b/proto/build.rs index 80894aa..a3e2fa4 100644 --- a/proto/build.rs +++ b/proto/build.rs @@ -1,7 +1,8 @@ use std::env; fn main() { - const DEFAULT_VERSION: &str = "v0.14.0-dev.2"; + // default Tenderdash version to use if TENDERDASH_COMMITISH is not set + const DEFAULT_VERSION: &str = "v0.14.0-dev.3"; // check if TENDERDASH_COMMITISH is already set; if not, set it to the current // version diff --git a/proto/src/error.rs b/proto/src/error.rs index a596665..cfa6eb4 100644 --- a/proto/src/error.rs +++ b/proto/src/error.rs @@ -1,7 +1,9 @@ //! This module defines the various errors that be raised during Protobuf //! conversions. - -use core::{fmt::Display, num::TryFromIntError}; +#[cfg(not(feature = "std"))] +use core::{convert::TryFrom, fmt::Display, num::TryFromIntError}; +#[cfg(feature = "std")] +use std::{fmt::Display, num::TryFromIntError}; use flex_error::{define_error, DisplayOnly}; use prost::{DecodeError, EncodeError}; diff --git a/proto/src/lib.rs b/proto/src/lib.rs index f5296db..ca3ad64 100644 --- a/proto/src/lib.rs +++ b/proto/src/lib.rs @@ -1,7 +1,7 @@ //! tenderdash-proto library gives the developer access to the Tenderdash //! proto-defined structs. -#![no_std] +#![cfg_attr(not(feature = "std"), no_std)] #![deny(warnings, trivial_casts, trivial_numeric_casts, unused_import_braces)] #![allow(clippy::large_enum_variant)] #![forbid(unsafe_code)] @@ -23,7 +23,13 @@ mod error; #[allow(warnings)] mod tenderdash; -use core::fmt::Display; +#[cfg(not(feature = "std"))] +use core::{ + convert::{TryFrom, TryInto}, + fmt::Display, +}; +#[cfg(feature = "std")] +use std::fmt::Display; use bytes::{Buf, BufMut}; pub use error::Error; @@ -34,6 +40,8 @@ pub mod serializers; use prelude::*; pub use tenderdash::meta::ABCI_VERSION; +#[cfg(feature = "grpc")] +pub use tonic; /// Allows for easy Google Protocol Buffers encoding and decoding of domain /// types with validation. diff --git a/proto/src/serializers/part_set_header_total.rs b/proto/src/serializers/part_set_header_total.rs index 3429550..17c9222 100644 --- a/proto/src/serializers/part_set_header_total.rs +++ b/proto/src/serializers/part_set_header_total.rs @@ -5,7 +5,11 @@ //! from a string-quoted integer value into an integer value without quotes in //! Tendermint Core v0.34.0. This deserializer allows backwards-compatibility by //! deserializing both ways. See also: -use core::fmt::Formatter; + +#[cfg(not(feature = "std"))] +use core::{convert::TryFrom, fmt::Formatter}; +#[cfg(feature = "std")] +use std::fmt::Formatter; use serde::{ de::{Error, Visitor}, diff --git a/proto/tests/unit.rs b/proto/tests/unit.rs index bd44a9b..e0fdfeb 100644 --- a/proto/tests/unit.rs +++ b/proto/tests/unit.rs @@ -1,3 +1,4 @@ +#[cfg(not(feature = "std"))] use core::convert::TryFrom; use tenderdash_proto::{ diff --git a/scripts/release.sh b/scripts/release.sh index fe5198e..0670b8d 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -1,6 +1,6 @@ #! /bin/bash -PLATFORM_DIR="$(realpath "$(dirname $0)/../../platform")" +PLATFORM_DIR="$(realpath "$(dirname "$0")/../../platform")" function help() { cat </dev/null if [ -d "$PLATFORM_DIR" ]; then rs_tenderdash="git = \"https:\/\/github.com\/dashpay\/rs-tenderdash-abci\", version = \"$rs_tenderdash_abci_version\" " echo "INFO: Updating references to tenderdash-abci / tenderdash-proto in $PLATFORM_DIR" - sed -i "s/^tenderdash-abci = { git = .* }/tenderdash-abci = { $rs_tenderdash }/" ${PLATFORM_DIR}/packages/*/Cargo.toml - sed -i "s/^tenderdash-proto = { git = .* }/tenderdash-proto = { $rs_tenderdash }/" ${PLATFORM_DIR}/packages/*/Cargo.toml + sed -i "s/^tenderdash-abci = { git = .* }/tenderdash-abci = { $rs_tenderdash }/" "${PLATFORM_DIR}"/packages/*/Cargo.toml + sed -i "s/^tenderdash-proto = { git = .* }/tenderdash-proto = { $rs_tenderdash }/" "${PLATFORM_DIR}"/packages/*/Cargo.toml else - echo WARN: Dash Platform not found in $PLATFORM_DIR, skipping + echo "WARN: Dash Platform not found in $PLATFORM_DIR, skipping" fi