zebra_checkpoints/
main.rs1use std::{ffi::OsString, process::Stdio};
12
13#[cfg(unix)]
14use std::os::unix::process::ExitStatusExt;
15
16use clap::Parser;
17use color_eyre::{
18 eyre::{ensure, eyre, Result},
19 Help,
20};
21use itertools::Itertools;
22use serde_json::Value;
23
24use zebra_chain::{
25 block::{self, Block, Height, HeightDiff, TryIntoHeight},
26 parameters::constants::MAX_BLOCK_REORG_HEIGHT,
27 serialization::ZcashDeserializeInto,
28 transparent::MIN_TRANSPARENT_COINBASE_MATURITY,
29};
30use zebra_node_services::{
31 constants::{MAX_CHECKPOINT_BYTE_COUNT, MAX_CHECKPOINT_HEIGHT_GAP},
32 rpc_client::RpcRequestClient,
33};
34use zebra_utils::init_tracing;
35
36const _: () = assert!(
40 MAX_BLOCK_REORG_HEIGHT >= MIN_TRANSPARENT_COINBASE_MATURITY,
41 "checkpoint settlement margin must be at least the coinbase maturity",
42);
43
44pub mod args;
45
46use args::{Args, Backend, Transport};
47
48async fn rpc_output<M, I>(our_args: &Args, method: M, params: I) -> Result<Value>
50where
51 M: AsRef<str>,
52 I: IntoIterator<Item = String>,
53{
54 match our_args.transport {
55 Transport::Cli => cli_output(our_args, method, params),
56 Transport::Direct => direct_output(our_args, method, params).await,
57 }
58}
59
60async fn direct_output<M, I>(our_args: &Args, method: M, params: I) -> Result<Value>
64where
65 M: AsRef<str>,
66 I: IntoIterator<Item = String>,
67{
68 let addr = our_args
70 .addr
71 .unwrap_or_else(|| "127.0.0.1:8232".parse().expect("valid address"));
72 let client = RpcRequestClient::new(addr);
73
74 let params = format!("[{}]", params.into_iter().join(", "));
79 let response = client.text_from_call(method, params).await?;
80
81 let mut response: Value = serde_json::from_str(&response)?;
83 let response = response["result"].take();
84
85 Ok(response)
86}
87
88fn cli_output<M, I>(our_args: &Args, method: M, params: I) -> Result<Value>
92where
93 M: AsRef<str>,
94 I: IntoIterator<Item = String>,
95{
96 let mut cmd = std::process::Command::new(&our_args.cli);
99 cmd.args(&our_args.zcli_args);
100
101 if let Some(addr) = our_args.addr {
103 cmd.arg(format!("-rpcconnect={}", addr.ip()));
104 cmd.arg(format!("-rpcport={}", addr.port()));
105 }
106
107 let method: OsString = method.as_ref().into();
109 cmd.arg(method);
110
111 for param in params {
112 let param = param.trim_matches('"');
115 let param: OsString = param.into();
116 cmd.arg(param);
117 }
118
119 let output = cmd.stderr(Stdio::inherit()).output()?;
121
122 #[cfg(unix)]
124 ensure!(
125 output.status.success(),
126 "Process failed: exit status {:?}, signal: {:?}",
127 output.status.code(),
128 output.status.signal()
129 );
130 #[cfg(not(unix))]
131 ensure!(
132 output.status.success(),
133 "Process failed: exit status {:?}",
134 output.status.code()
135 );
136
137 let response = String::from_utf8(output.stdout)?;
139 let response: Value = serde_json::from_str(&response)
142 .unwrap_or_else(|_error| Value::String(response.trim().to_string()));
143
144 Ok(response)
145}
146
147#[tokio::main]
149#[allow(clippy::print_stdout, clippy::print_stderr, clippy::unwrap_in_result)]
150async fn main() -> Result<()> {
151 eprintln!("zebra-checkpoints launched");
152
153 init_tracing();
155 color_eyre::install()?;
156
157 let args = args::Args::parse();
158
159 eprintln!("Command-line arguments: {args:?}");
160 eprintln!("Fetching block info and calculating checkpoints...\n\n");
161
162 let get_block_chain_info = rpc_output(&args, "getblockchaininfo", None)
164 .await
165 .with_suggestion(|| {
166 "Is the RPC server address and port correct? Is authentication configured correctly?"
167 })?;
168
169 let height_limit = get_block_chain_info["blocks"]
171 .try_into_height()
172 .expect("height: unexpected invalid value, missing field, or field type");
173
174 let height_limit = height_limit - HeightDiff::from(MAX_BLOCK_REORG_HEIGHT);
178 let height_limit = height_limit
179 .ok_or_else(|| {
180 eyre!(
181 "checkpoint generation needs at least {:?} blocks",
182 MAX_BLOCK_REORG_HEIGHT
183 )
184 })
185 .with_suggestion(|| "Hint: wait for the node to sync more blocks")?;
186
187 let starting_height = if let Some(last_checkpoint) = args.last_checkpoint {
190 (last_checkpoint + 1)
191 .expect("invalid last checkpoint height, must be less than the max height")
192 } else {
193 Height::MIN
194 };
195
196 assert!(
197 starting_height < height_limit,
198 "checkpoint generation needs more blocks than the starting height {starting_height:?}. \
199 Hint: wait for the node to sync more blocks"
200 );
201
202 let mut cumulative_bytes: u64 = 0;
204 let mut last_checkpoint_height = args.last_checkpoint.unwrap_or(Height::MIN);
205 let max_checkpoint_height_gap =
206 HeightDiff::try_from(MAX_CHECKPOINT_HEIGHT_GAP).expect("constant fits in HeightDiff");
207
208 for request_height in starting_height.0..height_limit.0 {
210 let (hash, response_height, size) = match args.backend {
213 Backend::Zcashd => {
214 let get_block = rpc_output(
216 &args,
217 "getblock",
218 [format!(r#""{request_height}""#), 1.to_string()],
219 )
220 .await?;
221
222 let hash: block::Hash = get_block["hash"]
224 .as_str()
225 .ok_or_else(|| eyre!("hash: unexpected missing field or field type"))?
226 .parse()?;
227 let response_height: Height =
228 get_block["height"].try_into_height().map_err(|_| {
229 eyre!("height: unexpected invalid value, missing field, or field type")
230 })?;
231
232 let size = get_block["size"].as_u64().ok_or_else(|| {
233 eyre!("size: unexpected invalid value, missing field, or field type")
234 })?;
235
236 (hash, response_height, size)
237 }
238 Backend::Zebrad => {
239 let block_bytes = rpc_output(
241 &args,
242 "getblock",
243 [format!(r#""{request_height}""#), 0.to_string()],
244 )
245 .await?;
246 let block_bytes = block_bytes
247 .as_str()
248 .ok_or_else(|| eyre!("block bytes: unexpected missing field or field type"))?;
249
250 let block_bytes: Vec<u8> = hex::decode(block_bytes)?;
251
252 let block: Block = block_bytes.zcash_deserialize_into()?;
259
260 (
261 block.hash(),
262 block
263 .coinbase_height()
264 .expect("valid blocks always have a coinbase height"),
265 block_bytes.len().try_into()?,
266 )
267 }
268 };
269
270 assert_eq!(
271 request_height, response_height.0,
272 "node returned a different block than requested"
273 );
274
275 cumulative_bytes += size;
277
278 let height_gap = response_height - last_checkpoint_height;
279
280 if response_height == Height::MIN
282 || cumulative_bytes >= MAX_CHECKPOINT_BYTE_COUNT
283 || height_gap >= max_checkpoint_height_gap
284 {
285 println!("{} {hash}", response_height.0);
287
288 cumulative_bytes = 0;
290 last_checkpoint_height = response_height;
291 }
292 }
293
294 Ok(())
295}