From 8b49c181d953f7326aef144051544a938c9ae83a Mon Sep 17 00:00:00 2001 From: Alfredo Garcia Date: Mon, 30 Sep 2024 18:36:30 -0300 Subject: [PATCH 1/5] add a cookie auth system for the rpc endpoint --- Cargo.lock | 1 + zebra-rpc/Cargo.toml | 3 ++ zebra-rpc/src/methods.rs | 12 +++++ zebra-rpc/src/server.rs | 7 ++- zebra-rpc/src/server/cookie.rs | 46 +++++++++++++++++ .../src/server/http_request_compatibility.rs | 49 +++++++++++++++++-- 6 files changed, 114 insertions(+), 4 deletions(-) create mode 100644 zebra-rpc/src/server/cookie.rs diff --git a/Cargo.lock b/Cargo.lock index 22f5d505038..c2abbfec8f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6164,6 +6164,7 @@ dependencies = [ name = "zebra-rpc" version = "1.0.0-beta.39" dependencies = [ + "base64 0.22.1", "chrono", "futures", "hex", diff --git a/zebra-rpc/Cargo.toml b/zebra-rpc/Cargo.toml index babae9123f1..2141b7a521b 100644 --- a/zebra-rpc/Cargo.toml +++ b/zebra-rpc/Cargo.toml @@ -68,6 +68,9 @@ jsonrpc-http-server = "18.0.0" serde_json = { version = "1.0.122", features = ["preserve_order"] } indexmap = { version = "2.3.0", features = ["serde"] } +# RPC endpoint basic auth +base64 = "0.22.1" + tokio = { version = "1.39.2", features = [ "time", "rt-multi-thread", diff --git a/zebra-rpc/src/methods.rs b/zebra-rpc/src/methods.rs index 8becc5bb79c..2952daa897e 100644 --- a/zebra-rpc/src/methods.rs +++ b/zebra-rpc/src/methods.rs @@ -314,6 +314,10 @@ pub trait Rpc { /// tags: control #[rpc(name = "stop")] fn stop(&self) -> Result; + + #[rpc(name = "unauthenticated")] + /// A dummy RPC method that just returns a non-authenticated RPC error. + fn unauthenticated(&self) -> Result<()>; } /// RPC method implementations. @@ -1383,6 +1387,14 @@ where data: None, }) } + + fn unauthenticated(&self) -> Result<()> { + Err(Error { + code: ErrorCode::ServerError(401), + message: "unauthenticated method".to_string(), + data: None, + }) + } } /// Returns the best chain tip height of `latest_chain_tip`, diff --git a/zebra-rpc/src/server.rs b/zebra-rpc/src/server.rs index b87068ef8f0..831812d28c5 100644 --- a/zebra-rpc/src/server.rs +++ b/zebra-rpc/src/server.rs @@ -33,6 +33,7 @@ use crate::{ #[cfg(feature = "getblocktemplate-rpcs")] use crate::methods::{GetBlockTemplateRpc, GetBlockTemplateRpcImpl}; +mod cookie; pub mod http_request_compatibility; pub mod rpc_call_compatibility; @@ -193,6 +194,9 @@ impl RpcServer { parallel_cpu_threads = available_parallelism().map(usize::from).unwrap_or(1); } + // generate a cookie + let generated_password = cookie::generate().unwrap_or("".to_string()); + // The server is a blocking task, which blocks on executor shutdown. // So we need to start it in a std::thread. // (Otherwise tokio panics on RPC port conflict, which shuts down the RPC server.) @@ -205,7 +209,7 @@ impl RpcServer { .threads(parallel_cpu_threads) // TODO: disable this security check if we see errors from lightwalletd //.allowed_hosts(DomainsValidation::Disabled) - .request_middleware(FixHttpRequestMiddleware) + .request_middleware(FixHttpRequestMiddleware::new(generated_password)) .start_http(&listen_addr) .expect("Unable to start RPC server"); @@ -299,6 +303,7 @@ impl RpcServer { span.in_scope(|| { info!("Stopping RPC server"); close_handle.clone().close(); + cookie::delete(); // delete the auth cookie debug!("Stopped RPC server"); }) }; diff --git a/zebra-rpc/src/server/cookie.rs b/zebra-rpc/src/server/cookie.rs new file mode 100644 index 00000000000..bab777f1d35 --- /dev/null +++ b/zebra-rpc/src/server/cookie.rs @@ -0,0 +1,46 @@ +//! Cookie-based authentication for the RPC server. + +use base64::Engine; +use rand::RngCore; + +use std::{ + fs::{remove_file, File}, + io::{Read, Write}, +}; + +/// The user field in the cookie (arbitrary, only for recognizability in debugging/logging purposes) +pub const COOKIEAUTH_USER: &str = "__cookie__"; +/// Default name for auth cookie file */ +const COOKIEAUTH_FILE: &str = ".cookie"; + +/// Generate a new auth cookie and return the encoded password. +pub fn generate() -> Option { + let mut data = [0u8; 32]; + rand::thread_rng().fill_bytes(&mut data); + let encoded_password = base64::prelude::BASE64_STANDARD.encode(data); + let cookie_content = format!("{}:{}", COOKIEAUTH_USER, encoded_password); + + let mut file = File::create(COOKIEAUTH_FILE).ok()?; + file.write_all(cookie_content.as_bytes()).ok()?; + + tracing::info!("RPC auth cookie generated successfully"); + + Some(encoded_password) +} + +/// Get the encoded password from the auth cookie. +pub fn get() -> Option { + let mut file = File::open(COOKIEAUTH_FILE).ok()?; + let mut contents = String::new(); + file.read_to_string(&mut contents).ok()?; + + let parts: Vec<&str> = contents.split(":").collect(); + Some(parts[1].to_string()) +} + +/// Delete the auth cookie. +pub fn delete() -> Option<()> { + remove_file(COOKIEAUTH_FILE).ok()?; + tracing::info!("RPC auth cookie deleted successfully"); + Some(()) +} diff --git a/zebra-rpc/src/server/http_request_compatibility.rs b/zebra-rpc/src/server/http_request_compatibility.rs index fede0e2bef0..79e82829e12 100644 --- a/zebra-rpc/src/server/http_request_compatibility.rs +++ b/zebra-rpc/src/server/http_request_compatibility.rs @@ -8,6 +8,8 @@ use jsonrpc_http_server::{ RequestMiddleware, RequestMiddlewareAction, }; +use crate::server::cookie; + /// HTTP [`RequestMiddleware`] with compatibility workarounds. /// /// This middleware makes the following changes to HTTP requests: @@ -34,8 +36,8 @@ use jsonrpc_http_server::{ /// Any user-specified data in RPC requests is hex or base58check encoded. /// We assume lightwalletd validates data encodings before sending it on to Zebra. /// So any fixes Zebra performs won't change user-specified data. -#[derive(Copy, Clone, Debug)] -pub struct FixHttpRequestMiddleware; +#[derive(Clone, Debug)] +pub struct FixHttpRequestMiddleware(String); impl RequestMiddleware for FixHttpRequestMiddleware { fn on_request(&self, mut request: Request) -> RequestMiddlewareAction { @@ -45,7 +47,7 @@ impl RequestMiddleware for FixHttpRequestMiddleware { FixHttpRequestMiddleware::insert_or_replace_content_type_header(request.headers_mut()); // Fix the request body - let request = request.map(|body| { + let mut request = request.map(|body| { let body = body.map_ok(|data| { // To simplify data handling, we assume that any search strings won't be split // across multiple `Bytes` data buffers. @@ -70,6 +72,18 @@ impl RequestMiddleware for FixHttpRequestMiddleware { Body::wrap_stream(body) }); + // Check if the request is authenticated + match cookie::get() { + Some(password) => { + if password != self.0 { + request = Self::unauthenticated(request); + } + } + None => { + request = Self::unauthenticated(request); + } + } + tracing::trace!(?request, "modified HTTP request"); RequestMiddlewareAction::Proceed { @@ -141,4 +155,33 @@ impl FixHttpRequestMiddleware { ); } } + + /// Create a new `FixHttpRequestMiddleware`. + pub fn new(password: String) -> Self { + Self(password) + } + + /// Change the method name in the JSON request. + fn change_method_name(data: String) -> String { + let mut json_data: serde_json::Value = serde_json::from_str(&data).expect("Invalid JSON"); + + if let Some(method) = json_data.get_mut("method") { + *method = serde_json::json!("unauthenticated"); + } + + serde_json::to_string(&json_data).expect("Failed to serialize JSON") + } + + /// Modify the request name to be `unauthenticated`. + fn unauthenticated(request: Request) -> Request { + request.map(|body| { + let body = body.map_ok(|data| { + let mut data = String::from_utf8_lossy(data.as_ref()).to_string(); + data = Self::change_method_name(data); + Bytes::from(data) + }); + + Body::wrap_stream(body) + }) + } } From 045048e704326d1f266910f45dcd539ac23744b7 Mon Sep 17 00:00:00 2001 From: Alfredo Garcia Date: Mon, 30 Sep 2024 19:40:37 -0300 Subject: [PATCH 2/5] fix rand import --- zebra-rpc/Cargo.toml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/zebra-rpc/Cargo.toml b/zebra-rpc/Cargo.toml index 2141b7a521b..38d028c1cf3 100644 --- a/zebra-rpc/Cargo.toml +++ b/zebra-rpc/Cargo.toml @@ -33,7 +33,6 @@ indexer-rpcs = [ # Mining RPC support getblocktemplate-rpcs = [ - "rand", "zcash_address", "zebra-consensus/getblocktemplate-rpcs", "zebra-state/getblocktemplate-rpcs", @@ -70,6 +69,7 @@ indexmap = { version = "2.3.0", features = ["serde"] } # RPC endpoint basic auth base64 = "0.22.1" +rand = "0.8.5" tokio = { version = "1.39.2", features = [ "time", @@ -95,8 +95,6 @@ nix = { version = "0.29.0", features = ["signal"] } zcash_primitives = { workspace = true, features = ["transparent-inputs"] } -# Experimental feature getblocktemplate-rpcs -rand = { version = "0.8.5", optional = true } # ECC deps used by getblocktemplate-rpcs feature zcash_address = { workspace = true, optional = true} From 212d51763f49aac6b86a60b97df7c38c9a569f3a Mon Sep 17 00:00:00 2001 From: Alfredo Garcia Date: Tue, 1 Oct 2024 19:15:57 -0300 Subject: [PATCH 3/5] fixes based on cookie method research --- zebra-rpc/src/server.rs | 4 +- zebra-rpc/src/server/cookie.rs | 8 +-- .../src/server/http_request_compatibility.rs | 50 ++++++++++++------- 3 files changed, 37 insertions(+), 25 deletions(-) diff --git a/zebra-rpc/src/server.rs b/zebra-rpc/src/server.rs index 831812d28c5..6f73a1e8482 100644 --- a/zebra-rpc/src/server.rs +++ b/zebra-rpc/src/server.rs @@ -195,7 +195,7 @@ impl RpcServer { } // generate a cookie - let generated_password = cookie::generate().unwrap_or("".to_string()); + cookie::generate(); // The server is a blocking task, which blocks on executor shutdown. // So we need to start it in a std::thread. @@ -209,7 +209,7 @@ impl RpcServer { .threads(parallel_cpu_threads) // TODO: disable this security check if we see errors from lightwalletd //.allowed_hosts(DomainsValidation::Disabled) - .request_middleware(FixHttpRequestMiddleware::new(generated_password)) + .request_middleware(FixHttpRequestMiddleware) .start_http(&listen_addr) .expect("Unable to start RPC server"); diff --git a/zebra-rpc/src/server/cookie.rs b/zebra-rpc/src/server/cookie.rs index bab777f1d35..dd413f39b95 100644 --- a/zebra-rpc/src/server/cookie.rs +++ b/zebra-rpc/src/server/cookie.rs @@ -1,6 +1,6 @@ //! Cookie-based authentication for the RPC server. -use base64::Engine; +use base64::{engine::general_purpose::URL_SAFE, Engine as _}; use rand::RngCore; use std::{ @@ -14,10 +14,10 @@ pub const COOKIEAUTH_USER: &str = "__cookie__"; const COOKIEAUTH_FILE: &str = ".cookie"; /// Generate a new auth cookie and return the encoded password. -pub fn generate() -> Option { +pub fn generate() -> Option<()> { let mut data = [0u8; 32]; rand::thread_rng().fill_bytes(&mut data); - let encoded_password = base64::prelude::BASE64_STANDARD.encode(data); + let encoded_password = URL_SAFE.encode(data); let cookie_content = format!("{}:{}", COOKIEAUTH_USER, encoded_password); let mut file = File::create(COOKIEAUTH_FILE).ok()?; @@ -25,7 +25,7 @@ pub fn generate() -> Option { tracing::info!("RPC auth cookie generated successfully"); - Some(encoded_password) + Some(()) } /// Get the encoded password from the auth cookie. diff --git a/zebra-rpc/src/server/http_request_compatibility.rs b/zebra-rpc/src/server/http_request_compatibility.rs index 79e82829e12..d676cfdacf2 100644 --- a/zebra-rpc/src/server/http_request_compatibility.rs +++ b/zebra-rpc/src/server/http_request_compatibility.rs @@ -2,6 +2,7 @@ //! //! These fixes are applied at the HTTP level, before the RPC request is parsed. +use base64::{engine::general_purpose::URL_SAFE, Engine as _}; use futures::TryStreamExt; use jsonrpc_http_server::{ hyper::{body::Bytes, header, Body, Request}, @@ -37,17 +38,22 @@ use crate::server::cookie; /// We assume lightwalletd validates data encodings before sending it on to Zebra. /// So any fixes Zebra performs won't change user-specified data. #[derive(Clone, Debug)] -pub struct FixHttpRequestMiddleware(String); +pub struct FixHttpRequestMiddleware; impl RequestMiddleware for FixHttpRequestMiddleware { fn on_request(&self, mut request: Request) -> RequestMiddlewareAction { tracing::trace!(?request, "original HTTP request"); + // Check if the request is authenticated + if !FixHttpRequestMiddleware::check_credentials(request.headers_mut()) { + request = Self::unauthenticated(request); + } + // Fix the request headers if needed and we can do so. FixHttpRequestMiddleware::insert_or_replace_content_type_header(request.headers_mut()); // Fix the request body - let mut request = request.map(|body| { + let request = request.map(|body| { let body = body.map_ok(|data| { // To simplify data handling, we assume that any search strings won't be split // across multiple `Bytes` data buffers. @@ -72,18 +78,6 @@ impl RequestMiddleware for FixHttpRequestMiddleware { Body::wrap_stream(body) }); - // Check if the request is authenticated - match cookie::get() { - Some(password) => { - if password != self.0 { - request = Self::unauthenticated(request); - } - } - None => { - request = Self::unauthenticated(request); - } - } - tracing::trace!(?request, "modified HTTP request"); RequestMiddlewareAction::Proceed { @@ -156,11 +150,6 @@ impl FixHttpRequestMiddleware { } } - /// Create a new `FixHttpRequestMiddleware`. - pub fn new(password: String) -> Self { - Self(password) - } - /// Change the method name in the JSON request. fn change_method_name(data: String) -> String { let mut json_data: serde_json::Value = serde_json::from_str(&data).expect("Invalid JSON"); @@ -184,4 +173,27 @@ impl FixHttpRequestMiddleware { Body::wrap_stream(body) }) } + + /// Check if the request is authenticated. + pub fn check_credentials(headers: &header::HeaderMap) -> bool { + headers + .get(header::AUTHORIZATION) + .and_then(|auth_header| auth_header.to_str().ok()) + .and_then(|auth| auth.split_whitespace().nth(1)) + .and_then(|token| URL_SAFE.decode(token).ok()) + .and_then(|decoded| String::from_utf8(decoded).ok()) + .and_then(|decoded_str| { + decoded_str + .split(':') + .nth(1) + .map(|password| password.to_string()) + }) + .map_or(false, |password| { + if let Some(cookie_password) = cookie::get() { + cookie_password == password + } else { + false + } + }) + } } From e91b1390f1d18635999327939a0e637346678c8e Mon Sep 17 00:00:00 2001 From: Alfredo Garcia Date: Mon, 7 Oct 2024 09:01:17 -0300 Subject: [PATCH 4/5] add and use `cookie_dir` config, rpc client changes --- Cargo.lock | 1 + zebra-node-services/Cargo.toml | 2 + zebra-node-services/src/rpc_client.rs | 12 ++- zebra-rpc/src/config.rs | 10 ++- zebra-rpc/src/server.rs | 6 +- zebra-rpc/src/server/cookie.rs | 11 +-- .../src/server/http_request_compatibility.rs | 8 +- zebra-rpc/src/server/tests/vectors.rs | 4 + zebra-rpc/src/sync.rs | 20 +++-- zebra-scan/src/bin/scanner/main.rs | 4 + zebra-utils/src/bin/zebra-checkpoints/main.rs | 4 +- zebrad/tests/acceptance.rs | 47 +++++++++-- zebrad/tests/common/cached_state.rs | 7 +- zebrad/tests/common/checkpoints.rs | 4 +- zebrad/tests/common/configs/v2.0.0-rc.0.toml | 83 +++++++++++++++++++ .../get_block_template.rs | 15 ++-- .../get_block_template_rpcs/get_peer_info.rs | 10 ++- .../get_block_template_rpcs/submit_block.rs | 5 +- zebrad/tests/common/lightwalletd/sync.rs | 5 +- zebrad/tests/common/regtest.rs | 6 +- zebrad/tests/common/test_type.rs | 1 - 21 files changed, 221 insertions(+), 44 deletions(-) create mode 100644 zebrad/tests/common/configs/v2.0.0-rc.0.toml diff --git a/Cargo.lock b/Cargo.lock index c2abbfec8f0..898df18d666 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6151,6 +6151,7 @@ dependencies = [ name = "zebra-node-services" version = "1.0.0-beta.39" dependencies = [ + "base64 0.22.1", "color-eyre", "jsonrpc-core", "reqwest", diff --git a/zebra-node-services/Cargo.toml b/zebra-node-services/Cargo.toml index 8d0992dcf5a..fec893c29ce 100644 --- a/zebra-node-services/Cargo.toml +++ b/zebra-node-services/Cargo.toml @@ -27,6 +27,7 @@ getblocktemplate-rpcs = [ # Tool and test features rpc-client = [ + "base64", "color-eyre", "jsonrpc-core", "reqwest", @@ -42,6 +43,7 @@ zebra-chain = { path = "../zebra-chain" , version = "1.0.0-beta.39" } # Optional dependencies # Tool and test feature rpc-client +base64 = { version = "0.22.1", optional = true } color-eyre = { version = "0.6.3", optional = true } jsonrpc-core = { version = "18.0.0", optional = true } # Security: avoid default dependency on openssl diff --git a/zebra-node-services/src/rpc_client.rs b/zebra-node-services/src/rpc_client.rs index 7f5ffbf192e..61c14b72c96 100644 --- a/zebra-node-services/src/rpc_client.rs +++ b/zebra-node-services/src/rpc_client.rs @@ -4,6 +4,7 @@ use std::net::SocketAddr; +use base64::{engine::general_purpose::URL_SAFE, Engine as _}; use reqwest::Client; use crate::BoxError; @@ -13,14 +14,16 @@ use crate::BoxError; pub struct RpcRequestClient { client: Client, rpc_address: SocketAddr, + auth_cookie: String, } impl RpcRequestClient { /// Creates new RPCRequestSender - pub fn new(rpc_address: SocketAddr) -> Self { + pub fn new(rpc_address: SocketAddr, auth_cookie: String) -> Self { Self { client: Client::new(), rpc_address, + auth_cookie, } } @@ -39,6 +42,13 @@ impl RpcRequestClient { r#"{{"jsonrpc": "2.0", "method": "{method}", "params": {params}, "id":123 }}"# )) .header("Content-Type", "application/json") + .header( + "Authorization", + format!( + "Basic {}", + URL_SAFE.encode(format!("__cookie__:{}", &self.auth_cookie)) + ), + ) .send() .await } diff --git a/zebra-rpc/src/config.rs b/zebra-rpc/src/config.rs index 8dc675b2034..ee26f679de3 100644 --- a/zebra-rpc/src/config.rs +++ b/zebra-rpc/src/config.rs @@ -1,9 +1,11 @@ //! User-configurable RPC settings. -use std::net::SocketAddr; +use std::{net::SocketAddr, path::PathBuf}; use serde::{Deserialize, Serialize}; +use zebra_chain::common::default_cache_dir; + pub mod mining; /// RPC configuration section. @@ -71,6 +73,9 @@ pub struct Config { /// Test-only option that makes Zebra say it is at the chain tip, /// no matter what the estimated height or local clock is. pub debug_force_finished_sync: bool, + + /// The directory where Zebra stores RPC cookies. + pub cookie_dir: PathBuf, } // This impl isn't derivable because it depends on features. @@ -94,6 +99,9 @@ impl Default for Config { // Debug options are always off by default. debug_force_finished_sync: false, + + // + cookie_dir: default_cache_dir(), } } } diff --git a/zebra-rpc/src/server.rs b/zebra-rpc/src/server.rs index 6f73a1e8482..a9b6dcfc6ac 100644 --- a/zebra-rpc/src/server.rs +++ b/zebra-rpc/src/server.rs @@ -33,7 +33,7 @@ use crate::{ #[cfg(feature = "getblocktemplate-rpcs")] use crate::methods::{GetBlockTemplateRpc, GetBlockTemplateRpcImpl}; -mod cookie; +pub mod cookie; pub mod http_request_compatibility; pub mod rpc_call_compatibility; @@ -195,7 +195,7 @@ impl RpcServer { } // generate a cookie - cookie::generate(); + cookie::generate(config.cookie_dir.clone()); // The server is a blocking task, which blocks on executor shutdown. // So we need to start it in a std::thread. @@ -209,7 +209,7 @@ impl RpcServer { .threads(parallel_cpu_threads) // TODO: disable this security check if we see errors from lightwalletd //.allowed_hosts(DomainsValidation::Disabled) - .request_middleware(FixHttpRequestMiddleware) + .request_middleware(FixHttpRequestMiddleware(config.clone())) .start_http(&listen_addr) .expect("Unable to start RPC server"); diff --git a/zebra-rpc/src/server/cookie.rs b/zebra-rpc/src/server/cookie.rs index dd413f39b95..cbcf7bd9f4e 100644 --- a/zebra-rpc/src/server/cookie.rs +++ b/zebra-rpc/src/server/cookie.rs @@ -6,21 +6,22 @@ use rand::RngCore; use std::{ fs::{remove_file, File}, io::{Read, Write}, + path::PathBuf, }; /// The user field in the cookie (arbitrary, only for recognizability in debugging/logging purposes) pub const COOKIEAUTH_USER: &str = "__cookie__"; /// Default name for auth cookie file */ -const COOKIEAUTH_FILE: &str = ".cookie"; +pub const COOKIEAUTH_FILE: &str = ".cookie"; /// Generate a new auth cookie and return the encoded password. -pub fn generate() -> Option<()> { +pub fn generate(cookie_dir: PathBuf) -> Option<()> { let mut data = [0u8; 32]; rand::thread_rng().fill_bytes(&mut data); let encoded_password = URL_SAFE.encode(data); let cookie_content = format!("{}:{}", COOKIEAUTH_USER, encoded_password); - let mut file = File::create(COOKIEAUTH_FILE).ok()?; + let mut file = File::create(cookie_dir.join(COOKIEAUTH_FILE)).ok()?; file.write_all(cookie_content.as_bytes()).ok()?; tracing::info!("RPC auth cookie generated successfully"); @@ -29,8 +30,8 @@ pub fn generate() -> Option<()> { } /// Get the encoded password from the auth cookie. -pub fn get() -> Option { - let mut file = File::open(COOKIEAUTH_FILE).ok()?; +pub fn get(cookie_dir: PathBuf) -> Option { + let mut file = File::open(cookie_dir.join(COOKIEAUTH_FILE)).ok()?; let mut contents = String::new(); file.read_to_string(&mut contents).ok()?; diff --git a/zebra-rpc/src/server/http_request_compatibility.rs b/zebra-rpc/src/server/http_request_compatibility.rs index d676cfdacf2..ac09e72e443 100644 --- a/zebra-rpc/src/server/http_request_compatibility.rs +++ b/zebra-rpc/src/server/http_request_compatibility.rs @@ -38,14 +38,14 @@ use crate::server::cookie; /// We assume lightwalletd validates data encodings before sending it on to Zebra. /// So any fixes Zebra performs won't change user-specified data. #[derive(Clone, Debug)] -pub struct FixHttpRequestMiddleware; +pub struct FixHttpRequestMiddleware(pub crate::config::Config); impl RequestMiddleware for FixHttpRequestMiddleware { fn on_request(&self, mut request: Request) -> RequestMiddlewareAction { tracing::trace!(?request, "original HTTP request"); // Check if the request is authenticated - if !FixHttpRequestMiddleware::check_credentials(request.headers_mut()) { + if !self.check_credentials(request.headers_mut()) { request = Self::unauthenticated(request); } @@ -175,7 +175,7 @@ impl FixHttpRequestMiddleware { } /// Check if the request is authenticated. - pub fn check_credentials(headers: &header::HeaderMap) -> bool { + pub fn check_credentials(&self, headers: &header::HeaderMap) -> bool { headers .get(header::AUTHORIZATION) .and_then(|auth_header| auth_header.to_str().ok()) @@ -189,7 +189,7 @@ impl FixHttpRequestMiddleware { .map(|password| password.to_string()) }) .map_or(false, |password| { - if let Some(cookie_password) = cookie::get() { + if let Some(cookie_password) = cookie::get(self.0.cookie_dir.clone()) { cookie_password == password } else { false diff --git a/zebra-rpc/src/server/tests/vectors.rs b/zebra-rpc/src/server/tests/vectors.rs index 9c50ecc7c35..ae670108f8d 100644 --- a/zebra-rpc/src/server/tests/vectors.rs +++ b/zebra-rpc/src/server/tests/vectors.rs @@ -46,6 +46,7 @@ fn rpc_server_spawn(parallel_cpu_threads: bool) { indexer_listen_addr: None, parallel_cpu_threads: if parallel_cpu_threads { 2 } else { 1 }, debug_force_finished_sync: false, + cookie_dir: Default::default(), }; let rt = tokio::runtime::Runtime::new().unwrap(); @@ -134,6 +135,7 @@ fn rpc_server_spawn_unallocated_port(parallel_cpu_threads: bool, do_shutdown: bo indexer_listen_addr: None, parallel_cpu_threads: if parallel_cpu_threads { 0 } else { 1 }, debug_force_finished_sync: false, + cookie_dir: Default::default(), }; let rt = tokio::runtime::Runtime::new().unwrap(); @@ -215,6 +217,7 @@ fn rpc_server_spawn_port_conflict() { indexer_listen_addr: None, parallel_cpu_threads: 1, debug_force_finished_sync: false, + cookie_dir: Default::default(), }; let rt = tokio::runtime::Runtime::new().unwrap(); @@ -326,6 +329,7 @@ fn rpc_server_spawn_port_conflict_parallel_auto() { indexer_listen_addr: None, parallel_cpu_threads: 2, debug_force_finished_sync: false, + cookie_dir: Default::default(), }; let rt = tokio::runtime::Runtime::new().unwrap(); diff --git a/zebra-rpc/src/sync.rs b/zebra-rpc/src/sync.rs index fd323ef64bb..0e3349b15bc 100644 --- a/zebra-rpc/src/sync.rs +++ b/zebra-rpc/src/sync.rs @@ -57,8 +57,10 @@ impl TrustedChainSync { rpc_address: SocketAddr, db: ZebraDb, non_finalized_state_sender: tokio::sync::watch::Sender, + cookie_dir: std::path::PathBuf, ) -> (LatestChainTip, ChainTipChange, JoinHandle<()>) { - let rpc_client = RpcRequestClient::new(rpc_address); + let auth_cookie = crate::server::cookie::get(cookie_dir).expect("cookie should exist"); + let rpc_client = RpcRequestClient::new(rpc_address, auth_cookie); let non_finalized_state = NonFinalizedState::new(&db.network()); let (chain_tip_sender, latest_chain_tip, chain_tip_change) = ChainTipSender::new(None, &db.network()); @@ -315,7 +317,8 @@ impl TrustedChainSync { /// Returns a [`ReadStateService`], [`LatestChainTip`], [`ChainTipChange`], and /// a [`JoinHandle`] for the sync task. pub fn init_read_state_with_syncer( - config: zebra_state::Config, + state_config: zebra_state::Config, + rpc_config: crate::config::Config, network: &Network, rpc_address: SocketAddr, ) -> tokio::task::JoinHandle< @@ -331,14 +334,19 @@ pub fn init_read_state_with_syncer( > { let network = network.clone(); tokio::spawn(async move { - if config.ephemeral { + if state_config.ephemeral { return Err("standalone read state service cannot be used with ephemeral state".into()); } let (read_state, db, non_finalized_state_sender) = - spawn_init_read_only(config, &network).await?; - let (latest_chain_tip, chain_tip_change, sync_task) = - TrustedChainSync::spawn(rpc_address, db, non_finalized_state_sender).await; + spawn_init_read_only(state_config, &network).await?; + let (latest_chain_tip, chain_tip_change, sync_task) = TrustedChainSync::spawn( + rpc_address, + db, + non_finalized_state_sender, + rpc_config.cookie_dir, + ) + .await; Ok((read_state, latest_chain_tip, chain_tip_change, sync_task)) }) } diff --git a/zebra-scan/src/bin/scanner/main.rs b/zebra-scan/src/bin/scanner/main.rs index e7ec853125f..092f0d2c1c5 100644 --- a/zebra-scan/src/bin/scanner/main.rs +++ b/zebra-scan/src/bin/scanner/main.rs @@ -7,6 +7,7 @@ use structopt::StructOpt; use tracing::*; use zebra_chain::{block::Height, parameters::Network}; +use zebra_rpc::config::Config as RpcConfig; use zebra_state::SaplingScanningKey; use core::net::SocketAddr; @@ -74,10 +75,13 @@ async fn main() -> Result<(), Box> { db_config, }; + let rpc_config = RpcConfig::default(); + // Get a read-only state and the database. let (read_state, _latest_chain_tip, chain_tip_change, sync_task) = zebra_rpc::sync::init_read_state_with_syncer( state_config, + rpc_config, &network, args.zebra_rpc_listen_addr, ) diff --git a/zebra-utils/src/bin/zebra-checkpoints/main.rs b/zebra-utils/src/bin/zebra-checkpoints/main.rs index cff158b5ca2..a2cffdf9b74 100644 --- a/zebra-utils/src/bin/zebra-checkpoints/main.rs +++ b/zebra-utils/src/bin/zebra-checkpoints/main.rs @@ -30,6 +30,7 @@ use zebra_node_services::{ constants::{MAX_CHECKPOINT_BYTE_COUNT, MAX_CHECKPOINT_HEIGHT_GAP}, rpc_client::RpcRequestClient, }; +use zebra_rpc::{config::Config as RpcConfig, server::cookie}; use zebra_utils::init_tracing; pub mod args; @@ -60,7 +61,8 @@ where let addr = our_args .addr .unwrap_or_else(|| "127.0.0.1:8232".parse().expect("valid address")); - let client = RpcRequestClient::new(addr); + let auth_cookie = cookie::get(RpcConfig::default().cookie_dir).expect("cookie should exist"); + let client = RpcRequestClient::new(addr, auth_cookie); // Launch a request with the RPC method and arguments // diff --git a/zebrad/tests/acceptance.rs b/zebrad/tests/acceptance.rs index cd3572ce3f2..b98996c95c4 100644 --- a/zebrad/tests/acceptance.rs +++ b/zebrad/tests/acceptance.rs @@ -164,7 +164,12 @@ use zebra_chain::{ }; use zebra_consensus::ParameterCheckpoint; use zebra_node_services::rpc_client::RpcRequestClient; -use zebra_rpc::server::OPENED_RPC_ENDPOINT_MSG; + +use zebra_rpc::{ + config::Config as RpcConfig, + server::{cookie, OPENED_RPC_ENDPOINT_MSG}, +}; + use zebra_state::{constants::LOCK_FILE_ERROR, state_database_format_version_in_code}; #[cfg(not(target_os = "windows"))] @@ -1591,8 +1596,11 @@ async fn rpc_endpoint(parallel_cpu_threads: bool) -> Result<()> { // Wait until port is open. let rpc_address = read_listen_addr_from_logs(&mut child, OPENED_RPC_ENDPOINT_MSG)?; + // Get the auth cookie + let auth_cookie = + zebra_rpc::server::cookie::get(config.rpc.cookie_dir).expect("cookie should exist"); // Create an http client - let client = RpcRequestClient::new(rpc_address); + let client = RpcRequestClient::new(rpc_address, auth_cookie); // Make the call to the `getinfo` RPC method let res = client.call("getinfo", "[]".to_string()).await?; @@ -1648,8 +1656,12 @@ async fn rpc_endpoint_client_content_type() -> Result<()> { // Wait until port is open. let rpc_address = read_listen_addr_from_logs(&mut child, OPENED_RPC_ENDPOINT_MSG)?; + // Get the auth cookie + let auth_cookie = + zebra_rpc::server::cookie::get(config.rpc.cookie_dir).expect("cookie should exist"); + // Create an http client - let client = RpcRequestClient::new(rpc_address); + let client = RpcRequestClient::new(rpc_address, auth_cookie); // Call to `getinfo` RPC method with a no content type. let res = client @@ -1734,8 +1746,12 @@ fn non_blocking_logger() -> Result<()> { // Wait until port is open. let rpc_address = read_listen_addr_from_logs(&mut child, OPENED_RPC_ENDPOINT_MSG)?; + // Get the auth cookie + let auth_cookie = + zebra_rpc::server::cookie::get(config.rpc.cookie_dir).expect("cookie should exist"); + // Create an http client - let client = RpcRequestClient::new(rpc_address); + let client = RpcRequestClient::new(rpc_address, auth_cookie); // 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. @@ -2406,7 +2422,10 @@ async fn fully_synced_rpc_test() -> Result<()> { zebrad.expect_stdout_line_matches(format!("Opened RPC endpoint at {zebra_rpc_address}"))?; - let client = RpcRequestClient::new(zebra_rpc_address); + // Get the auth cookie + let auth_cookie = cookie::get(RpcConfig::default().cookie_dir).expect("cookie should exist"); + + let client = RpcRequestClient::new(zebra_rpc_address, auth_cookie); // Make a getblock test that works only on synced node (high block number). // The block is before the mandatory checkpoint, so the checkpoint cached state can be used @@ -2846,9 +2865,14 @@ async fn fully_synced_rpc_z_getsubtreesbyindex_snapshot_test() -> Result<()> { // Wait for zebrad to load the full cached blockchain. zebrad.expect_stdout_line_matches(SYNC_FINISHED_REGEX)?; + // Get the auth cookie + let auth_cookie = cookie::get(RpcConfig::default().cookie_dir).expect("cookie should exist"); + // Create an http client - let client = - RpcRequestClient::new(zebra_rpc_address.expect("already checked that address is valid")); + let client = RpcRequestClient::new( + zebra_rpc_address.expect("already checked that address is valid"), + auth_cookie, + ); // Create test vector matrix let zcashd_test_vectors = vec![ @@ -2999,7 +3023,8 @@ async fn trusted_chain_sync_handles_forks_correctly() -> Result<()> { // Spawn a read state with the RPC syncer to check that it has the same best chain as Zebra let (read_state, _latest_chain_tip, mut chain_tip_change, _sync_task) = zebra_rpc::sync::init_read_state_with_syncer( - config.state, + config.state.clone(), + config.rpc, &config.network.network, rpc_address, ) @@ -3017,7 +3042,10 @@ async fn trusted_chain_sync_handles_forks_correctly() -> Result<()> { tracing::info!("got genesis chain tip change, submitting more blocks .."); - let rpc_client = RpcRequestClient::new(rpc_address); + // Get the auth cookie + let auth_cookie = cookie::get(config.state.cache_dir).expect("cookie should exist"); + + let rpc_client = RpcRequestClient::new(rpc_address, auth_cookie); let mut blocks = Vec::new(); for _ in 0..10 { let (block, height) = rpc_client.submit_block_from_template().await?; @@ -3211,6 +3239,7 @@ async fn trusted_chain_sync_handles_forks_correctly() -> Result<()> { let (_read_state, _latest_chain_tip, mut chain_tip_change, _sync_task) = zebra_rpc::sync::init_read_state_with_syncer( config.state, + config.rpc, &config.network.network, rpc_address, ) diff --git a/zebrad/tests/common/cached_state.rs b/zebrad/tests/common/cached_state.rs index 58f6064cdf5..ac00e471b77 100644 --- a/zebrad/tests/common/cached_state.rs +++ b/zebrad/tests/common/cached_state.rs @@ -22,6 +22,8 @@ use zebra_chain::{ serialization::ZcashDeserializeInto, }; use zebra_node_services::rpc_client::RpcRequestClient; + +use zebra_rpc::{config::Config as RpcConfig, server::cookie}; use zebra_state::{ChainTipChange, LatestChainTip, MAX_BLOCK_REORG_HEIGHT}; use zebra_test::command::TestChild; @@ -228,8 +230,11 @@ pub async fn get_raw_future_blocks( true, )?; + // Get the auth cookie + let auth_cookie = cookie::get(RpcConfig::default().cookie_dir).expect("cookie should exist"); + // Create an http client - let rpc_client = RpcRequestClient::new(rpc_address); + let rpc_client = RpcRequestClient::new(rpc_address, auth_cookie); let blockchain_info: serde_json::Value = serde_json::from_str( &rpc_client diff --git a/zebrad/tests/common/checkpoints.rs b/zebrad/tests/common/checkpoints.rs index c1c0ae44716..2ac45e808c0 100644 --- a/zebrad/tests/common/checkpoints.rs +++ b/zebrad/tests/common/checkpoints.rs @@ -20,6 +20,7 @@ use zebra_chain::{ }; use zebra_consensus::MAX_CHECKPOINT_HEIGHT_GAP; use zebra_node_services::rpc_client::RpcRequestClient; +use zebra_rpc::{config::Config as RpcConfig, server::cookie}; use zebra_state::state_database_format_version_in_code; use zebra_test::{ args, @@ -504,7 +505,8 @@ pub fn wait_for_zebra_checkpoints_generation< /// Returns an approximate `zebrad` tip height, using JSON-RPC. #[tracing::instrument] pub async fn zebrad_tip_height(zebra_rpc_address: SocketAddr) -> Result { - let client = RpcRequestClient::new(zebra_rpc_address); + let auth_cookie = cookie::get(RpcConfig::default().cookie_dir).expect("cookie should exist"); + let client = RpcRequestClient::new(zebra_rpc_address, auth_cookie); let zebrad_blockchain_info = client .text_from_call("getblockchaininfo", "[]".to_string()) diff --git a/zebrad/tests/common/configs/v2.0.0-rc.0.toml b/zebrad/tests/common/configs/v2.0.0-rc.0.toml new file mode 100644 index 00000000000..70ba534e611 --- /dev/null +++ b/zebrad/tests/common/configs/v2.0.0-rc.0.toml @@ -0,0 +1,83 @@ +# Default configuration for zebrad. +# +# This file can be used as a skeleton for custom configs. +# +# Unspecified fields use default values. Optional fields are Some(field) if the +# field is present and None if it is absent. +# +# This file is generated as an example using zebrad's current defaults. +# You should set only the config options you want to keep, and delete the rest. +# Only a subset of fields are present in the skeleton, since optional values +# whose default is None are omitted. +# +# The config format (including a complete list of sections and fields) is +# documented here: +# https://docs.rs/zebrad/latest/zebrad/config/struct.ZebradConfig.html +# +# zebrad attempts to load configs in the following order: +# +# 1. The -c flag on the command line, e.g., `zebrad -c myconfig.toml start`; +# 2. The file `zebrad.toml` in the users's preference directory (platform-dependent); +# 3. The default config. +# +# The user's preference directory and the default path to the `zebrad` config are platform dependent, +# based on `dirs::preference_dir`, see https://docs.rs/dirs/latest/dirs/fn.preference_dir.html : +# +# | Platform | Value | Example | +# | -------- | ------------------------------------- | ---------------------------------------------- | +# | Linux | `$XDG_CONFIG_HOME` or `$HOME/.config` | `/home/alice/.config/zebrad.toml` | +# | macOS | `$HOME/Library/Preferences` | `/Users/Alice/Library/Preferences/zebrad.toml` | +# | Windows | `{FOLDERID_RoamingAppData}` | `C:\Users\Alice\AppData\Local\zebrad.toml` | + +[consensus] +checkpoint_sync = true + +[mempool] +eviction_memory_time = "1h" +tx_cost_limit = 80000000 + +[metrics] + +[mining] +debug_like_zcashd = true + +[network] +cache_dir = true +crawl_new_peer_interval = "1m 1s" +initial_mainnet_peers = [ + "dnsseed.z.cash:8233", + "dnsseed.str4d.xyz:8233", + "mainnet.seeder.zfnd.org:8233", + "mainnet.is.yolo.money:8233", +] +initial_testnet_peers = [ + "dnsseed.testnet.z.cash:18233", + "testnet.seeder.zfnd.org:18233", + "testnet.is.yolo.money:18233", +] +listen_addr = "0.0.0.0:8233" +max_connections_per_ip = 1 +network = "Mainnet" +peerset_initial_target_size = 25 + +[rpc] +cookie_dir = "cache_dir" +debug_force_finished_sync = false +parallel_cpu_threads = 0 + +[state] +cache_dir = "cache_dir" +delete_old_database = true +ephemeral = false + +[sync] +checkpoint_verify_concurrency_limit = 1000 +download_concurrency_limit = 50 +full_verify_concurrency_limit = 20 +parallel_cpu_threads = 0 + +[tracing] +buffer_limit = 128000 +force_use_color = false +use_color = true +use_journald = false diff --git a/zebrad/tests/common/get_block_template_rpcs/get_block_template.rs b/zebrad/tests/common/get_block_template_rpcs/get_block_template.rs index b13c7f04236..442c8d2dca4 100644 --- a/zebrad/tests/common/get_block_template_rpcs/get_block_template.rs +++ b/zebrad/tests/common/get_block_template_rpcs/get_block_template.rs @@ -16,11 +16,15 @@ use zebra_chain::{ serialization::ZcashSerialize, }; use zebra_node_services::rpc_client::RpcRequestClient; -use zebra_rpc::methods::get_block_template_rpcs::{ - get_block_template::{ - proposal::TimeSource, GetBlockTemplate, JsonParameters, ProposalResponse, +use zebra_rpc::{ + config::Config as RpcConfig, + methods::get_block_template_rpcs::{ + get_block_template::{ + proposal::TimeSource, GetBlockTemplate, JsonParameters, ProposalResponse, + }, + types::get_block_template::proposal_block_from_template, }, - types::get_block_template::proposal_block_from_template, + server::cookie, }; use crate::common::{ @@ -94,7 +98,8 @@ pub(crate) async fn run() -> Result<()> { true, )?; - let client = RpcRequestClient::new(rpc_address); + let auth_cookie = cookie::get(RpcConfig::default().cookie_dir).expect("cookie should exist"); + let client = RpcRequestClient::new(rpc_address, auth_cookie); tracing::info!( "calling getblocktemplate RPC method at {rpc_address}, \ diff --git a/zebrad/tests/common/get_block_template_rpcs/get_peer_info.rs b/zebrad/tests/common/get_block_template_rpcs/get_peer_info.rs index dd30954948c..e745ef9cead 100644 --- a/zebrad/tests/common/get_block_template_rpcs/get_peer_info.rs +++ b/zebrad/tests/common/get_block_template_rpcs/get_peer_info.rs @@ -4,7 +4,10 @@ use color_eyre::eyre::{eyre, Context, Result}; use zebra_chain::parameters::Network; use zebra_node_services::rpc_client::RpcRequestClient; -use zebra_rpc::methods::get_block_template_rpcs::types::peer_info::PeerInfo; +use zebra_rpc::{ + config::Config as RpcConfig, methods::get_block_template_rpcs::types::peer_info::PeerInfo, + server::cookie, +}; use crate::common::{ launch::{can_spawn_zebrad_for_test_type, spawn_zebrad_for_rpc}, @@ -38,8 +41,11 @@ pub(crate) async fn run() -> Result<()> { tracing::info!(?rpc_address, "zebrad opened its RPC port",); + // Get the auth cookie + let auth_cookie = cookie::get(RpcConfig::default().cookie_dir).expect("cookie should exist"); + // call `getpeerinfo` RPC method - let peer_info_result: Vec = RpcRequestClient::new(rpc_address) + let peer_info_result: Vec = RpcRequestClient::new(rpc_address, auth_cookie) .json_result_from_call("getpeerinfo", "[]".to_string()) .await .map_err(|err| eyre!(err))?; diff --git a/zebrad/tests/common/get_block_template_rpcs/submit_block.rs b/zebrad/tests/common/get_block_template_rpcs/submit_block.rs index 399efc8d99e..e02227102ac 100644 --- a/zebrad/tests/common/get_block_template_rpcs/submit_block.rs +++ b/zebrad/tests/common/get_block_template_rpcs/submit_block.rs @@ -12,6 +12,7 @@ use color_eyre::eyre::{Context, Result}; use zebra_chain::parameters::Network; use zebra_node_services::rpc_client::RpcRequestClient; +use zebra_rpc::{config::Config as RpcConfig, server::cookie}; use crate::common::{ cached_state::get_raw_future_blocks, @@ -63,8 +64,10 @@ pub(crate) async fn run() -> Result<()> { tracing::info!(?rpc_address, "zebrad opened its RPC port",); + let auth_cookie = cookie::get(RpcConfig::default().cookie_dir).expect("cookie should exist"); + // Create an http client - let client = RpcRequestClient::new(rpc_address); + let client = RpcRequestClient::new(rpc_address, auth_cookie); for raw_block in raw_blocks { let res = client diff --git a/zebrad/tests/common/lightwalletd/sync.rs b/zebrad/tests/common/lightwalletd/sync.rs index 8dce05a9150..f9d3a72e027 100644 --- a/zebrad/tests/common/lightwalletd/sync.rs +++ b/zebrad/tests/common/lightwalletd/sync.rs @@ -9,6 +9,7 @@ use std::{ use tempfile::TempDir; use zebra_node_services::rpc_client::RpcRequestClient; +use zebra_rpc::{config::Config as RpcConfig, server::cookie}; use zebra_test::prelude::*; use crate::common::{launch::ZebradTestDirExt, test_type::TestType}; @@ -204,7 +205,9 @@ pub fn are_zebrad_and_lightwalletd_tips_synced( let lightwalletd_tip_height = (lightwalletd_next_height - 1) as u64; // Get the block tip from zebrad - let client = RpcRequestClient::new(zebra_rpc_address); + let auth_cookie = + cookie::get(RpcConfig::default().cookie_dir).expect("cookie should exist"); + let client = RpcRequestClient::new(zebra_rpc_address, auth_cookie); let zebrad_blockchain_info = client .text_from_call("getblockchaininfo", "[]".to_string()) .await?; diff --git a/zebrad/tests/common/regtest.rs b/zebrad/tests/common/regtest.rs index bf1cba697de..fae5c57e7ab 100644 --- a/zebrad/tests/common/regtest.rs +++ b/zebrad/tests/common/regtest.rs @@ -17,6 +17,7 @@ use zebra_chain::{ }; use zebra_node_services::rpc_client::RpcRequestClient; use zebra_rpc::{ + config::Config as RpcConfig, constants::MISSING_BLOCK_ERROR_CODE, methods::{ get_block_template_rpcs::{ @@ -27,7 +28,7 @@ use zebra_rpc::{ }, hex_data::HexData, }, - server::OPENED_RPC_ENDPOINT_MSG, + server::{cookie, OPENED_RPC_ENDPOINT_MSG}, }; use zebra_test::args; @@ -73,7 +74,8 @@ pub(crate) async fn submit_blocks_test() -> Result<()> { /// Get block templates and submit blocks async fn submit_blocks(network: Network, rpc_address: SocketAddr) -> Result<()> { - let client = RpcRequestClient::new(rpc_address); + let auth_cookie = cookie::get(RpcConfig::default().cookie_dir).expect("cookie should exist"); + let client = RpcRequestClient::new(rpc_address, auth_cookie); for _ in 1..=NUM_BLOCKS_TO_SUBMIT { let (mut block, height) = client diff --git a/zebrad/tests/common/test_type.rs b/zebrad/tests/common/test_type.rs index bc0cd8ef417..7b1199cf365 100644 --- a/zebrad/tests/common/test_type.rs +++ b/zebrad/tests/common/test_type.rs @@ -220,7 +220,6 @@ impl TestType { config.sync.checkpoint_verify_concurrency_limit = zebrad::components::sync::DEFAULT_CHECKPOINT_CONCURRENCY_LIMIT; } - Some(Ok(config)) } From efdbd2a6df670ced8fe5367bd724d62e232f988f Mon Sep 17 00:00:00 2001 From: Alfredo Garcia Date: Mon, 7 Oct 2024 17:11:26 -0300 Subject: [PATCH 5/5] add missing dependency --- zebra-node-services/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/zebra-node-services/Cargo.toml b/zebra-node-services/Cargo.toml index fec893c29ce..d199e9abbf1 100644 --- a/zebra-node-services/Cargo.toml +++ b/zebra-node-services/Cargo.toml @@ -54,6 +54,7 @@ tokio = { version = "1.39.2", features = ["time", "sync"] } [dev-dependencies] +base64 = "0.22.1" color-eyre = "0.6.3" jsonrpc-core = "18.0.0" reqwest = { version = "0.11.26", default-features = false, features = ["rustls-tls"] }