Skip to main content

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 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
36// Checkpoints are generated past Zebra's rollback window (`MAX_BLOCK_REORG_HEIGHT`),
37// which must be at least the coinbase maturity so that checkpointed coinbase outputs
38// are already settled.
39const _: () = 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
48/// Make an RPC call based on `our_args` and `rpc_command`, and return the response as a [`Value`].
49async 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
60/// Connect to the node with `our_args` and `rpc_command`, and return the response as a [`Value`].
61///
62/// Only used if the transport is [`Direct`](Transport::Direct).
63async 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    // Get a new RPC client that will connect to our node
69    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    // Launch a request with the RPC method and arguments
75    //
76    // The params are a JSON array with typed arguments.
77    // TODO: accept JSON value arguments, and do this formatting using serde_json
78    let params = format!("[{}]", params.into_iter().join(", "));
79    let response = client.text_from_call(method, params).await?;
80
81    // Extract the "result" field from the RPC response
82    let mut response: Value = serde_json::from_str(&response)?;
83    let response = response["result"].take();
84
85    Ok(response)
86}
87
88/// Run `cmd` with `our_args` and `rpc_command`, and return its output as a [`Value`].
89///
90/// Only used if the transport is [`Cli`](Transport::Cli).
91fn 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    // Get a new `zcash-cli` command configured for our node,
97    // including the `zebra-checkpoints` passthrough arguments.
98    let mut cmd = std::process::Command::new(&our_args.cli);
99    cmd.args(&our_args.zcli_args);
100
101    // Turn the address into command-line arguments
102    if let Some(addr) = our_args.addr {
103        cmd.arg(format!("-rpcconnect={}", addr.ip()));
104        cmd.arg(format!("-rpcport={}", addr.port()));
105    }
106
107    // Add the RPC method and arguments
108    let method: OsString = method.as_ref().into();
109    cmd.arg(method);
110
111    for param in params {
112        // Remove JSON string/int type formatting, because zcash-cli will add it anyway
113        // TODO: accept JSON value arguments, and do this formatting using serde_json?
114        let param = param.trim_matches('"');
115        let param: OsString = param.into();
116        cmd.arg(param);
117    }
118
119    // Launch a CLI request, capturing stdout, but sending stderr to the user
120    let output = cmd.stderr(Stdio::inherit()).output()?;
121
122    // Make sure the command was successful
123    #[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    // Make sure the output is valid UTF-8 JSON
138    let response = String::from_utf8(output.stdout)?;
139    // zcash-cli returns raw strings without JSON type info.
140    // As a workaround, assume that invalid responses are strings.
141    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/// Process entry point for `zebra-checkpoints`
148#[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    // initialise
154    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    // get the current block count
163    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    // calculate the maximum height
170    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    // Checkpoints must be on a settled part of the best chain, so we skip blocks
175    // within Zebra's rollback window (`MAX_BLOCK_REORG_HEIGHT`). A smaller margin
176    // could let a reorg that Zebra would still follow orphan a shipped checkpoint.
177    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    // Start at the next block after the last checkpoint.
188    // If there is no last checkpoint, start at genesis (height 0).
189    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    // set up counters
203    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    // loop through all blocks
209    for request_height in starting_height.0..height_limit.0 {
210        // In `Cli` transport mode we need to create a process for each block
211
212        let (hash, response_height, size) = match args.backend {
213            Backend::Zcashd => {
214                // get block data from zcashd using verbose=1
215                let get_block = rpc_output(
216                    &args,
217                    "getblock",
218                    [format!(r#""{request_height}""#), 1.to_string()],
219                )
220                .await?;
221
222                // get the values we are interested in
223                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                // get block data from zebrad (or zcashd) by deserializing the raw block
240                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                // TODO: is it faster to call both `getblock height verbosity=0`
253                //       and `getblock height verbosity=1`, rather than deserializing the block
254                //       and calculating its hash?
255                //
256                // It seems to be fast enough for checkpoint updates for now,
257                // but generating the full list takes more than an hour.
258                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        // compute cumulative totals
276        cumulative_bytes += size;
277
278        let height_gap = response_height - last_checkpoint_height;
279
280        // check if this block should be a checkpoint
281        if response_height == Height::MIN
282            || cumulative_bytes >= MAX_CHECKPOINT_BYTE_COUNT
283            || height_gap >= max_checkpoint_height_gap
284        {
285            // print to output
286            println!("{} {hash}", response_height.0);
287
288            // reset cumulative totals
289            cumulative_bytes = 0;
290            last_checkpoint_height = response_height;
291        }
292    }
293
294    Ok(())
295}