Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .github/workflows/ci-basic.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Basic checks

#on: [push, pull_request]
on: [push]

jobs:
test:
name: Test on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-24.04]

env:
# Use system-installed RocksDB library instead of building from scratch
ROCKSDB_LIB_DIR: /usr/lib
# Use system-installed Snappy library for compression in RocksDB
SNAPPY_LIB_DIR: /usr/lib/x86_64-linux-gnu

steps:
- uses: actions/checkout@v4
- name: Install dependencies on Ubuntu
#run: sudo apt-get update && sudo apt-get install -y protobuf-compiler build-essential librocksdb-dev
run: sudo apt-get update && sudo apt-get install -y protobuf-compiler librocksdb-dev
- name: Install formatting & linting tools
run: rustup component add rustfmt clippy
- name: Run tests
env:
RUST_BACKTRACE: 1
# Skip two tests that intermittently fail on CI (likely a race/ordering issue).
# FIXME: investigate and fix the underlying flake; remove these skips once resolved.
run: cargo test --locked --verbose -- --skip v4_with_sapling_spends --skip v5_with_sapling_spends
# Run the skipped tests separately with constrained parallelism
- name: Run Sapling spend tests
run: |
cargo test -p zebra-consensus v4_with_sapling_spends -- --nocapture
cargo test -p zebra-consensus v5_with_sapling_spends -- --nocapture
- name: Verify working directory is clean
run: git diff --exit-code
- name: Run doc check
run: cargo doc --all-features --document-private-items
- name: Run format check
run: cargo fmt -- --check
- name: Run clippy
run: cargo clippy --workspace --all-features --all-targets -- -D warnings -A mismatched-lifetime-syntaxes
55 changes: 55 additions & 0 deletions zebra-rpc/src/methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,20 @@ pub trait Rpc {
address_strings: AddressStrings,
) -> Result<GetAddressUtxosResponse>;

/// Returns node health and build metadata, as a [`GetHealthInfo`] JSON struct.
///
/// zcashd reference: none
/// method: post
/// tags: control
///
/// # Notes
///
/// - This method provides a simple liveness/readiness signal and basic build info.
/// - When the HTTP health endpoint is enabled in HTTP middleware,
/// it is also available as `GET /health` (no parameters).
#[method(name = "gethealthinfo")]
fn get_health_info(&self) -> Result<GetHealthInfo>;

/// Stop the running zebrad process.
///
/// # Notes
Expand Down Expand Up @@ -2059,6 +2073,10 @@ where
Ok(response_utxos)
}

fn get_health_info(&self) -> Result<GetHealthInfo> {
Ok(GetHealthInfo::new())
}

fn stop(&self) -> Result<String> {
#[cfg(not(target_os = "windows"))]
if self.network.is_regtest() {
Expand Down Expand Up @@ -2910,6 +2928,43 @@ where
.ok_or_misc_error("No blocks in state")
}

/// Response to a `gethealthinfo` RPC request.
///
/// See the notes for the [`Rpc::get_health_info` method].
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct GetHealthInfo {
/// Static health status string.
status: String,
/// Zebra package version
version: String,
/// Build Git tag (if available).
git_tag: String,
/// Full Git commit hash (if available).
git_commit: String,
/// Server timestamp in RFC 3339 format.
timestamp: String,
}

impl Default for GetHealthInfo {
fn default() -> Self {
Self::new()
}
}

impl GetHealthInfo {
/// Creates a new health info instance with current node status and build metadata.
#[inline]
pub fn new() -> Self {
Self {
status: "healthy".into(),
version: env!("CARGO_PKG_VERSION").into(),
git_tag: option_env!("GIT_TAG").unwrap_or("unknown").into(),
git_commit: option_env!("GIT_COMMIT_FULL").unwrap_or("unknown").into(),
timestamp: Utc::now().to_rfc3339(),
}
}
}

/// Response to a `getinfo` RPC request.
///
/// See the notes for the [`Rpc::get_info` method].
Expand Down
15 changes: 15 additions & 0 deletions zebra-rpc/src/methods/tests/snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,12 @@ async fn test_rpc_response_data_for_network(network: &Network) {
.await
.expect("We should have a vector of strings");
snapshot_rpc_getaddressutxos(get_address_utxos, &settings);

// `gethealthinfo`
let get_health_info = rpc
.get_health_info()
.expect("We should have a GetHealthInfo struct");
snapshot_rpc_gethealthinfo(get_health_info, &settings);
}

async fn test_mocked_rpc_response_data_for_network(network: &Network) {
Expand Down Expand Up @@ -798,6 +804,15 @@ fn snapshot_rpc_getaddressutxos(utxos: Vec<Utxo>, settings: &insta::Settings) {
settings.bind(|| insta::assert_json_snapshot!("get_address_utxos", utxos));
}

/// Snapshot `gethealthinfo` response, using `cargo insta` and JSON serialization.
fn snapshot_rpc_gethealthinfo(info: GetHealthInfo, settings: &insta::Settings) {
// Snapshot only the `status` field since other fields vary per build/run.
let status_only = serde_json::json!({ "status": info.status });
settings.bind(|| {
insta::assert_json_snapshot!("get_health_info_status", status_only);
});
}

/// Snapshot `getblockcount` response, using `cargo insta` and JSON serialization.
fn snapshot_rpc_getblockcount(block_count: u32, settings: &insta::Settings) {
settings.bind(|| insta::assert_json_snapshot!("get_block_count", block_count));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
source: zebra-rpc/src/methods/tests/snapshot.rs
assertion_line: 808
expression: status_only
---
{
"status": "healthy"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
source: zebra-rpc/src/methods/tests/snapshot.rs
assertion_line: 808
expression: status_only
---
{
"status": "healthy"
}
31 changes: 22 additions & 9 deletions zebra-rpc/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
use std::{fmt, panic};

use cookie::Cookie;
use jsonrpsee::server::{middleware::rpc::RpcServiceBuilder, Server, ServerHandle};
use jsonrpsee::server::{
middleware::{http::ProxyGetRequestLayer, rpc::RpcServiceBuilder},
Server, ServerHandle,
};
use tokio::task::JoinHandle;
use tower::Service;
use tracing::*;
Expand Down Expand Up @@ -147,16 +150,26 @@ impl RpcServer {
.listen_addr
.expect("caller should make sure listen_addr is set");

let http_middleware_layer = if conf.enable_cookie_auth {
let cookie = Cookie::default();
cookie::write_to_disk(&cookie, &conf.cookie_dir)
.expect("Zebra must be able to write the auth cookie to the disk");
HttpRequestMiddlewareLayer::new(Some(cookie))
} else {
HttpRequestMiddlewareLayer::new(None)
let http_middleware_layer = match conf.enable_cookie_auth {
true => {
let cookie = Cookie::default();
match cookie::write_to_disk(&cookie, &conf.cookie_dir) {
Ok(_) => HttpRequestMiddlewareLayer::new(Some(cookie)),
Err(err) => {
error!(?err, "Failed to write auth cookie to disk");
return Err(err.into());
}
}
}
false => HttpRequestMiddlewareLayer::new(None),
};

let http_middleware = tower::ServiceBuilder::new().layer(http_middleware_layer);
let health_proxy_layer = ProxyGetRequestLayer::new("/health", "gethealthinfo")
.map_err(Into::into)?;

let http_middleware = tower::ServiceBuilder::new()
.layer(health_proxy_layer)
.layer(http_middleware_layer);

let rpc_middleware = RpcServiceBuilder::new()
.rpc_logger(1024)
Expand Down
23 changes: 23 additions & 0 deletions zebrad/tests/acceptance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1634,6 +1634,20 @@ async fn rpc_endpoint_client_content_type() -> Result<()> {
// Create an http client
let client = RpcRequestClient::new(rpc_address);

// Just test with plain content type, similar to getinfo.
let res = client
.call_with_content_type(
"gethealthinfo",
"[]".to_string(),
"application/json".to_string(),
)
.await?;
assert!(res.status().is_success());

let body = res.bytes().await?;
let parsed: Value = serde_json::from_slice(&body)?;
assert_eq!(parsed["result"]["status"], "healthy");

// Call to `getinfo` RPC method with a no content type.
let res = client
.call_with_no_content_type("getinfo", "[]".to_string())
Expand Down Expand Up @@ -1718,6 +1732,15 @@ fn non_blocking_logger() -> Result<()> {
// Create an http client
let client = RpcRequestClient::new(rpc_address);

// Make the call to the `gethealthinfo` RPC method.
let res = client.call("gethealthinfo", "[]".to_string()).await?;
assert!(res.status().is_success());

let body = res.bytes().await?;
let parsed: Value = serde_json::from_slice(&body)?;
let status = parsed["result"]["status"].as_str().unwrap();
assert_eq!(status, "healthy");

// Most of Zebra's lines are 100-200 characters long, so 500 requests should print enough to fill the unix pipe,
// fill the channel that tracing logs are queued onto, and drop logs rather than block execution.
for _ in 0..500 {
Expand Down
Loading