zebra_checkpoints/
main.rs

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.
10
11use std::{ffi::OsString, process::Stdio};
12
13#[cfg(unix)]
14use std::os::unix::process::ExitStatusExt;
15
16use color_eyre::{
17    eyre::{ensure, eyre, Result},
18    Help,
19};
20use itertools::Itertools;
21use serde_json::Value;
22use structopt::StructOpt;
23
24use 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;
34
35pub mod args;
36
37use args::{Args, Backend, Transport};
38
39/// 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
42    M: AsRef<str>,
43    I: IntoIterator<Item = String>,
44{
45    match our_args.transport {
46        Transport::Cli => cli_output(our_args, method, params),
47        Transport::Direct => direct_output(our_args, method, params).await,
48    }
49}
50
51/// 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
56    M: AsRef<str>,
57    I: IntoIterator<Item = String>,
58{
59    // Get a new RPC client that will connect to our node
60    let addr = our_args
61        .addr
62        .unwrap_or_else(|| "127.0.0.1:8232".parse().expect("valid address"));
63    let client = RpcRequestClient::new(addr);
64
65    // 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
69    let params = format!("[{}]", params.into_iter().join(", "));
70    let response = client.text_from_call(method, params).await?;
71
72    // Extract the "result" field from the RPC response
73    let mut response: Value = serde_json::from_str(&response)?;
74    let response = response["result"].take();
75
76    Ok(response)
77}
78
79/// 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
84    M: 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.
89    let mut cmd = std::process::Command::new(&our_args.cli);
90    cmd.args(&our_args.zcli_args);
91
92    // Turn the address into command-line arguments
93    if let Some(addr) = our_args.addr {
94        cmd.arg(format!("-rpcconnect={}", addr.ip()));
95        cmd.arg(format!("-rpcport={}", addr.port()));
96    }
97
98    // Add the RPC method and arguments
99    let method: OsString = method.as_ref().into();
100    cmd.arg(method);
101
102    for 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?
105        let param = param.trim_matches('"');
106        let param: OsString = param.into();
107        cmd.arg(param);
108    }
109
110    // Launch a CLI request, capturing stdout, but sending stderr to the user
111    let output = cmd.stderr(Stdio::inherit()).output()?;
112
113    // Make sure the command was successful
114    #[cfg(unix)]
115    ensure!(
116        output.status.success(),
117        "Process failed: exit status {:?}, signal: {:?}",
118        output.status.code(),
119        output.status.signal()
120    );
121    #[cfg(not(unix))]
122    ensure!(
123        output.status.success(),
124        "Process failed: exit status {:?}",
125        output.status.code()
126    );
127
128    // Make sure the output is valid UTF-8 JSON
129    let 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.
132    let response: Value = serde_json::from_str(&response)
133        .unwrap_or_else(|_error| Value::String(response.trim().to_string()));
134
135    Ok(response)
136}
137
138/// Process entry point for `zebra-checkpoints`
139#[tokio::main]
140#[allow(clippy::print_stdout, clippy::print_stderr)]
141async fn main() -> Result<()> {
142    eprintln!("zebra-checkpoints launched");
143
144    // initialise
145    init_tracing();
146    color_eyre::install()?;
147
148    let args = args::Args::from_args();
149
150    eprintln!("Command-line arguments: {args:?}");
151    eprintln!("Fetching block info and calculating checkpoints...\n\n");
152
153    // get the current block count
154    let 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        })?;
159
160    // calculate the maximum height
161    let height_limit = get_block_chain_info["blocks"]
162        .try_into_height()
163        .expect("height: unexpected invalid value, missing field, or field type");
164
165    // Checkpoints must be on the main chain, so we skip blocks that are within the
166    // Zcash reorg limit.
167    let height_limit = height_limit - HeightDiff::from(MIN_TRANSPARENT_COINBASE_MATURITY);
168    let height_limit = height_limit
169        .ok_or_else(|| {
170            eyre!(
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")?;
176
177    // Start at the next block after the last checkpoint.
178    // If there is no last checkpoint, start at genesis (height 0).
179    let 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    };
185
186    assert!(
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    );
191
192    // set up counters
193    let mut cumulative_bytes: u64 = 0;
194    let mut last_checkpoint_height = args.last_checkpoint.unwrap_or(Height::MIN);
195    let max_checkpoint_height_gap =
196        HeightDiff::try_from(MAX_CHECKPOINT_HEIGHT_GAP).expect("constant fits in HeightDiff");
197
198    // loop through all blocks
199    for request_height in starting_height.0..height_limit.0 {
200        // In `Cli` transport mode we need to create a process for each block
201
202        let (hash, response_height, size) = match args.backend {
203            Backend::Zcashd => {
204                // get block data from zcashd using verbose=1
205                let get_block = rpc_output(
206                    &args,
207                    "getblock",
208                    [format!(r#""{request_height}""#), 1.to_string()],
209                )
210                .await?;
211
212                // get the values we are interested in
213                let hash: block::Hash = get_block["hash"]
214                    .as_str()
215                    .expect("hash: unexpected missing field or field type")
216                    .parse()?;
217                let response_height: Height = get_block["height"]
218                    .try_into_height()
219                    .expect("height: unexpected invalid value, missing field, or field type");
220
221                let size = get_block["size"]
222                    .as_u64()
223                    .expect("size: unexpected invalid value, missing field, or field type");
224
225                (hash, response_height, size)
226            }
227            Backend::Zebrad => {
228                // get block data from zebrad (or zcashd) by deserializing the raw block
229                let block_bytes = rpc_output(
230                    &args,
231                    "getblock",
232                    [format!(r#""{request_height}""#), 0.to_string()],
233                )
234                .await?;
235                let block_bytes = block_bytes
236                    .as_str()
237                    .expect("block bytes: unexpected missing field or field type");
238
239                let block_bytes: Vec<u8> = hex::decode(block_bytes)?;
240
241                // 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.
247                let block: Block = block_bytes.zcash_deserialize_into()?;
248
249                (
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        };
258
259        assert_eq!(
260            request_height, response_height.0,
261            "node returned a different block than requested"
262        );
263
264        // compute cumulative totals
265        cumulative_bytes += size;
266
267        let height_gap = response_height - last_checkpoint_height;
268
269        // check if this block should be a checkpoint
270        if response_height == Height::MIN
271            || cumulative_bytes >= MAX_CHECKPOINT_BYTE_COUNT
272            || height_gap >= max_checkpoint_height_gap
273        {
274            // print to output
275            println!("{} {hash}", response_height.0);
276
277            // reset cumulative totals
278            cumulative_bytes = 0;
279            last_checkpoint_height = response_height;
280        }
281    }
282
283    Ok(())
284}