1//! Prints Zebra checkpoints as "height hash" output lines.
2//!
3//! Get all the blocks up to network current tip and print the ones that are
4//! checkpoints according to rules.
5//!
6//! For usage please refer to the program help: `zebra-checkpoints --help`
7//!
8//! zebra-consensus accepts an ordered list of checkpoints, starting with the
9//! genesis block. Checkpoint heights can be chosen arbitrarily.
1011use std::{ffi::OsString, process::Stdio};
1213#[cfg(unix)]
14use std::os::unix::process::ExitStatusExt;
1516use color_eyre::{
17 eyre::{ensure, eyre, Result},
18 Help,
19};
20use itertools::Itertools;
21use serde_json::Value;
22use structopt::StructOpt;
2324use zebra_chain::{
25 block::{self, Block, Height, HeightDiff, TryIntoHeight},
26 serialization::ZcashDeserializeInto,
27 transparent::MIN_TRANSPARENT_COINBASE_MATURITY,
28};
29use zebra_node_services::{
30 constants::{MAX_CHECKPOINT_BYTE_COUNT, MAX_CHECKPOINT_HEIGHT_GAP},
31 rpc_client::RpcRequestClient,
32};
33use zebra_utils::init_tracing;
3435pub mod args;
3637use args::{Args, Backend, Transport};
3839/// Make an RPC call based on `our_args` and `rpc_command`, and return the response as a [`Value`].
40async fn rpc_output<M, I>(our_args: &Args, method: M, params: I) -> Result<Value>
41where
42M: AsRef<str>,
43 I: IntoIterator<Item = String>,
44{
45match our_args.transport {
46 Transport::Cli => cli_output(our_args, method, params),
47 Transport::Direct => direct_output(our_args, method, params).await,
48 }
49}
5051/// Connect to the node with `our_args` and `rpc_command`, and return the response as a [`Value`].
52///
53/// Only used if the transport is [`Direct`](Transport::Direct).
54async fn direct_output<M, I>(our_args: &Args, method: M, params: I) -> Result<Value>
55where
56M: AsRef<str>,
57 I: IntoIterator<Item = String>,
58{
59// Get a new RPC client that will connect to our node
60let addr = our_args
61 .addr
62 .unwrap_or_else(|| "127.0.0.1:8232".parse().expect("valid address"));
63let client = RpcRequestClient::new(addr);
6465// Launch a request with the RPC method and arguments
66 //
67 // The params are a JSON array with typed arguments.
68 // TODO: accept JSON value arguments, and do this formatting using serde_json
69let params = format!("[{}]", params.into_iter().join(", "));
70let response = client.text_from_call(method, params).await?;
7172// Extract the "result" field from the RPC response
73let mut response: Value = serde_json::from_str(&response)?;
74let response = response["result"].take();
7576Ok(response)
77}
7879/// Run `cmd` with `our_args` and `rpc_command`, and return its output as a [`Value`].
80///
81/// Only used if the transport is [`Cli`](Transport::Cli).
82fn cli_output<M, I>(our_args: &Args, method: M, params: I) -> Result<Value>
83where
84M: AsRef<str>,
85 I: IntoIterator<Item = String>,
86{
87// Get a new `zcash-cli` command configured for our node,
88 // including the `zebra-checkpoints` passthrough arguments.
89let mut cmd = std::process::Command::new(&our_args.cli);
90 cmd.args(&our_args.zcli_args);
9192// Turn the address into command-line arguments
93if let Some(addr) = our_args.addr {
94 cmd.arg(format!("-rpcconnect={}", addr.ip()));
95 cmd.arg(format!("-rpcport={}", addr.port()));
96 }
9798// Add the RPC method and arguments
99let method: OsString = method.as_ref().into();
100 cmd.arg(method);
101102for param in params {
103// Remove JSON string/int type formatting, because zcash-cli will add it anyway
104 // TODO: accept JSON value arguments, and do this formatting using serde_json?
105let param = param.trim_matches('"');
106let param: OsString = param.into();
107 cmd.arg(param);
108 }
109110// Launch a CLI request, capturing stdout, but sending stderr to the user
111let output = cmd.stderr(Stdio::inherit()).output()?;
112113// Make sure the command was successful
114#[cfg(unix)]
115ensure!(
116 output.status.success(),
117"Process failed: exit status {:?}, signal: {:?}",
118 output.status.code(),
119 output.status.signal()
120 );
121#[cfg(not(unix))]
122ensure!(
123 output.status.success(),
124"Process failed: exit status {:?}",
125 output.status.code()
126 );
127128// Make sure the output is valid UTF-8 JSON
129let response = String::from_utf8(output.stdout)?;
130// zcash-cli returns raw strings without JSON type info.
131 // As a workaround, assume that invalid responses are strings.
132let response: Value = serde_json::from_str(&response)
133 .unwrap_or_else(|_error| Value::String(response.trim().to_string()));
134135Ok(response)
136}
137138/// Process entry point for `zebra-checkpoints`
139#[tokio::main]
140#[allow(clippy::print_stdout, clippy::print_stderr)]
141async fn main() -> Result<()> {
142eprintln!("zebra-checkpoints launched");
143144// initialise
145init_tracing();
146 color_eyre::install()?;
147148let args = args::Args::from_args();
149150eprintln!("Command-line arguments: {args:?}");
151eprintln!("Fetching block info and calculating checkpoints...\n\n");
152153// get the current block count
154let get_block_chain_info = rpc_output(&args, "getblockchaininfo", None)
155 .await
156.with_suggestion(|| {
157"Is the RPC server address and port correct? Is authentication configured correctly?"
158})?;
159160// calculate the maximum height
161let height_limit = get_block_chain_info["blocks"]
162 .try_into_height()
163 .expect("height: unexpected invalid value, missing field, or field type");
164165// Checkpoints must be on the main chain, so we skip blocks that are within the
166 // Zcash reorg limit.
167let height_limit = height_limit - HeightDiff::from(MIN_TRANSPARENT_COINBASE_MATURITY);
168let height_limit = height_limit
169 .ok_or_else(|| {
170eyre!(
171"checkpoint generation needs at least {:?} blocks",
172 MIN_TRANSPARENT_COINBASE_MATURITY
173 )
174 })
175 .with_suggestion(|| "Hint: wait for the node to sync more blocks")?;
176177// Start at the next block after the last checkpoint.
178 // If there is no last checkpoint, start at genesis (height 0).
179let starting_height = if let Some(last_checkpoint) = args.last_checkpoint {
180 (last_checkpoint + 1)
181 .expect("invalid last checkpoint height, must be less than the max height")
182 } else {
183 Height::MIN
184 };
185186assert!(
187 starting_height < height_limit,
188"checkpoint generation needs more blocks than the starting height {starting_height:?}. \
189 Hint: wait for the node to sync more blocks"
190);
191192// set up counters
193let mut cumulative_bytes: u64 = 0;
194let mut last_checkpoint_height = args.last_checkpoint.unwrap_or(Height::MIN);
195let max_checkpoint_height_gap =
196 HeightDiff::try_from(MAX_CHECKPOINT_HEIGHT_GAP).expect("constant fits in HeightDiff");
197198// loop through all blocks
199for request_height in starting_height.0..height_limit.0 {
200// In `Cli` transport mode we need to create a process for each block
201202let (hash, response_height, size) = match args.backend {
203 Backend::Zcashd => {
204// get block data from zcashd using verbose=1
205let get_block = rpc_output(
206&args,
207"getblock",
208 [format!(r#""{request_height}""#), 1.to_string()],
209 )
210 .await?;
211212// get the values we are interested in
213let hash: block::Hash = get_block["hash"]
214 .as_str()
215 .expect("hash: unexpected missing field or field type")
216 .parse()?;
217let response_height: Height = get_block["height"]
218 .try_into_height()
219 .expect("height: unexpected invalid value, missing field, or field type");
220221let size = get_block["size"]
222 .as_u64()
223 .expect("size: unexpected invalid value, missing field, or field type");
224225 (hash, response_height, size)
226 }
227 Backend::Zebrad => {
228// get block data from zebrad (or zcashd) by deserializing the raw block
229let block_bytes = rpc_output(
230&args,
231"getblock",
232 [format!(r#""{request_height}""#), 0.to_string()],
233 )
234 .await?;
235let block_bytes = block_bytes
236 .as_str()
237 .expect("block bytes: unexpected missing field or field type");
238239let block_bytes: Vec<u8> = hex::decode(block_bytes)?;
240241// TODO: is it faster to call both `getblock height verbosity=0`
242 // and `getblock height verbosity=1`, rather than deserializing the block
243 // and calculating its hash?
244 //
245 // It seems to be fast enough for checkpoint updates for now,
246 // but generating the full list takes more than an hour.
247let block: Block = block_bytes.zcash_deserialize_into()?;
248249 (
250 block.hash(),
251 block
252 .coinbase_height()
253 .expect("valid blocks always have a coinbase height"),
254 block_bytes.len().try_into()?,
255 )
256 }
257 };
258259assert_eq!(
260 request_height, response_height.0,
261"node returned a different block than requested"
262);
263264// compute cumulative totals
265cumulative_bytes += size;
266267let height_gap = response_height - last_checkpoint_height;
268269// check if this block should be a checkpoint
270if response_height == Height::MIN
271 || cumulative_bytes >= MAX_CHECKPOINT_BYTE_COUNT
272 || height_gap >= max_checkpoint_height_gap
273 {
274// print to output
275println!("{} {hash}", response_height.0);
276277// reset cumulative totals
278cumulative_bytes = 0;
279 last_checkpoint_height = response_height;
280 }
281 }
282283Ok(())
284}