use std::{ffi::OsString, process::Stdio};
#[cfg(unix)]
use std::os::unix::process::ExitStatusExt;
use color_eyre::{
eyre::{ensure, eyre, Result},
Help,
};
use itertools::Itertools;
use serde_json::Value;
use structopt::StructOpt;
use zebra_chain::{
block::{self, Block, Height, HeightDiff, TryIntoHeight},
serialization::ZcashDeserializeInto,
transparent::MIN_TRANSPARENT_COINBASE_MATURITY,
};
use zebra_node_services::{
constants::{MAX_CHECKPOINT_BYTE_COUNT, MAX_CHECKPOINT_HEIGHT_GAP},
rpc_client::RpcRequestClient,
};
use zebra_utils::init_tracing;
pub mod args;
use args::{Args, Backend, Transport};
async fn rpc_output<M, I>(our_args: &Args, method: M, params: I) -> Result<Value>
where
M: AsRef<str>,
I: IntoIterator<Item = String>,
{
match our_args.transport {
Transport::Cli => cli_output(our_args, method, params),
Transport::Direct => direct_output(our_args, method, params).await,
}
}
async fn direct_output<M, I>(our_args: &Args, method: M, params: I) -> Result<Value>
where
M: AsRef<str>,
I: IntoIterator<Item = String>,
{
let addr = our_args
.addr
.unwrap_or_else(|| "127.0.0.1:8232".parse().expect("valid address"));
let client = RpcRequestClient::new(addr);
let params = format!("[{}]", params.into_iter().join(", "));
let response = client.text_from_call(method, params).await?;
let mut response: Value = serde_json::from_str(&response)?;
let response = response["result"].take();
Ok(response)
}
fn cli_output<M, I>(our_args: &Args, method: M, params: I) -> Result<Value>
where
M: AsRef<str>,
I: IntoIterator<Item = String>,
{
let mut cmd = std::process::Command::new(&our_args.cli);
cmd.args(&our_args.zcli_args);
if let Some(addr) = our_args.addr {
cmd.arg(format!("-rpcconnect={}", addr.ip()));
cmd.arg(format!("-rpcport={}", addr.port()));
}
let method: OsString = method.as_ref().into();
cmd.arg(method);
for param in params {
let param = param.trim_matches('"');
let param: OsString = param.into();
cmd.arg(param);
}
let output = cmd.stderr(Stdio::inherit()).output()?;
#[cfg(unix)]
ensure!(
output.status.success(),
"Process failed: exit status {:?}, signal: {:?}",
output.status.code(),
output.status.signal()
);
#[cfg(not(unix))]
ensure!(
output.status.success(),
"Process failed: exit status {:?}",
output.status.code()
);
let response = String::from_utf8(output.stdout)?;
let response: Value = serde_json::from_str(&response)
.unwrap_or_else(|_error| Value::String(response.trim().to_string()));
Ok(response)
}
#[tokio::main]
#[allow(clippy::print_stdout, clippy::print_stderr, clippy::unwrap_in_result)]
async fn main() -> Result<()> {
eprintln!("zebra-checkpoints launched");
init_tracing();
color_eyre::install()?;
let args = args::Args::from_args();
eprintln!("Command-line arguments: {args:?}");
eprintln!("Fetching block info and calculating checkpoints...\n\n");
let get_block_chain_info = rpc_output(&args, "getblockchaininfo", None)
.await
.with_suggestion(|| {
"Is the RPC server address and port correct? Is authentication configured correctly?"
})?;
let height_limit = get_block_chain_info["blocks"]
.try_into_height()
.expect("height: unexpected invalid value, missing field, or field type");
let height_limit = height_limit - HeightDiff::from(MIN_TRANSPARENT_COINBASE_MATURITY);
let height_limit = height_limit
.ok_or_else(|| {
eyre!(
"checkpoint generation needs at least {:?} blocks",
MIN_TRANSPARENT_COINBASE_MATURITY
)
})
.with_suggestion(|| "Hint: wait for the node to sync more blocks")?;
let starting_height = if let Some(last_checkpoint) = args.last_checkpoint {
(last_checkpoint + 1)
.expect("invalid last checkpoint height, must be less than the max height")
} else {
Height::MIN
};
assert!(
starting_height < height_limit,
"checkpoint generation needs more blocks than the starting height {starting_height:?}. \
Hint: wait for the node to sync more blocks"
);
let mut cumulative_bytes: u64 = 0;
let mut last_checkpoint_height = args.last_checkpoint.unwrap_or(Height::MIN);
let max_checkpoint_height_gap =
HeightDiff::try_from(MAX_CHECKPOINT_HEIGHT_GAP).expect("constant fits in HeightDiff");
for request_height in starting_height.0..height_limit.0 {
let (hash, response_height, size) = match args.backend {
Backend::Zcashd => {
let get_block = rpc_output(
&args,
"getblock",
[format!(r#""{request_height}""#), 1.to_string()],
)
.await?;
let hash: block::Hash = get_block["hash"]
.as_str()
.ok_or_else(|| eyre!("hash: unexpected missing field or field type"))?
.parse()?;
let response_height: Height =
get_block["height"].try_into_height().map_err(|_| {
eyre!("height: unexpected invalid value, missing field, or field type")
})?;
let size = get_block["size"].as_u64().ok_or_else(|| {
eyre!("size: unexpected invalid value, missing field, or field type")
})?;
(hash, response_height, size)
}
Backend::Zebrad => {
let block_bytes = rpc_output(
&args,
"getblock",
[format!(r#""{request_height}""#), 0.to_string()],
)
.await?;
let block_bytes = block_bytes
.as_str()
.ok_or_else(|| eyre!("block bytes: unexpected missing field or field type"))?;
let block_bytes: Vec<u8> = hex::decode(block_bytes)?;
let block: Block = block_bytes.zcash_deserialize_into()?;
(
block.hash(),
block
.coinbase_height()
.expect("valid blocks always have a coinbase height"),
block_bytes.len().try_into()?,
)
}
};
assert_eq!(
request_height, response_height.0,
"node returned a different block than requested"
);
cumulative_bytes += size;
let height_gap = response_height - last_checkpoint_height;
if response_height == Height::MIN
|| cumulative_bytes >= MAX_CHECKPOINT_BYTE_COUNT
|| height_gap >= max_checkpoint_height_gap
{
println!("{} {hash}", response_height.0);
cumulative_bytes = 0;
last_checkpoint_height = response_height;
}
}
Ok(())
}