From 6a06cbf3ad3a718d1f67c05c2543b5c9e4625a33 Mon Sep 17 00:00:00 2001 From: teor Date: Wed, 18 Jan 2023 12:11:15 +1000 Subject: [PATCH] test(rpc): Create a script that submits block proposals to zcashd (#5944) * Revert "Update code that we're going to delete in the next PR anyway" This reverts commit 1fed70da9ef6f891d6465bcfc0ec4bacd9e81948. * Initial zcash-test-block-template script without block proposal construction * Try creating block data using jq and printf * Move proposal_block_from_template() to zebra-rpc and remove eyre dependency * Implement FromStr for DateTime32 and Duration32 * Basic block-template-to-proposal command without time source handling * Move block proposal code into its own module * Use time source in block-template-to-proposal * Make block-template-to-proposal require the getblocktemplate-rpcs feature * Use block-template-to-proposal in the test script zcash-rpc-block-template-to-proposal * Apply new hex formatting to commitments and nonces in block proposal tests * Re-add missing imports from rebase * Re-add missing conversions from rebase * Update to new method name after rebase * Derive a Default impl * Restore a comment that was accidentally deleted during the rebase * Avoid a clippy::unwrap-in-result * Temporarily fix the code for a disabled test * Fix tool build with Docker caches * Make clippy happy with the temporary fix * Give a pass/fail status for each proposal response * Accept zcashd block templates in block-template-to-proposal * Fix pretty printing * Zebra expects a longpollid, so give it a dummy value * Add "required" fields which Zebra requires * Use curtime as the dummy maxtime in zcashd templates * Support large block proposals by reading proposal data from a file * Test all valid time modes for each proposal * Allow the user to set the time command * Put debug logs into their own files * Exit with an error status when any proposal is invalid * Log the date and time to make it easier to match errors to node logs --- Cargo.lock | 1 + zebra-chain/src/serialization/date_time.rs | 28 ++- .../types/get_block_template.rs | 6 +- .../types/get_block_template/proposal.rs | 159 +++++++++++- zebra-utils/Cargo.toml | 24 ++ .../bin/block-template-to-proposal/args.rs | 25 ++ .../bin/block-template-to-proposal/main.rs | 112 +++++++++ .../zcash-rpc-block-template-to-proposal | 227 ++++++++++++++++++ .../get_block_template.rs | 75 +----- 9 files changed, 585 insertions(+), 72 deletions(-) create mode 100644 zebra-utils/src/bin/block-template-to-proposal/args.rs create mode 100644 zebra-utils/src/bin/block-template-to-proposal/main.rs create mode 100755 zebra-utils/zcash-rpc-block-template-to-proposal diff --git a/Cargo.lock b/Cargo.lock index 17bf1ce16e4..fbf772951c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5633,6 +5633,7 @@ dependencies = [ "tracing-subscriber 0.3.16", "zebra-chain", "zebra-node-services", + "zebra-rpc", ] [[package]] diff --git a/zebra-chain/src/serialization/date_time.rs b/zebra-chain/src/serialization/date_time.rs index 5b2c0f73837..def9dfa5ef5 100644 --- a/zebra-chain/src/serialization/date_time.rs +++ b/zebra-chain/src/serialization/date_time.rs @@ -1,11 +1,15 @@ //! DateTime types with specific serialization invariants. -use std::{fmt, num::TryFromIntError}; +use std::{ + fmt, + num::{ParseIntError, TryFromIntError}, + str::FromStr, +}; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use chrono::{TimeZone, Utc}; -use super::{SerializationError, ZcashDeserialize, ZcashSerialize}; +use crate::serialization::{SerializationError, ZcashDeserialize, ZcashSerialize}; /// A date and time, represented by a 32-bit number of seconds since the UNIX epoch. #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] @@ -347,6 +351,26 @@ impl TryFrom<&std::time::Duration> for Duration32 { } } +impl FromStr for DateTime32 { + type Err = ParseIntError; + + fn from_str(s: &str) -> Result { + Ok(DateTime32 { + timestamp: s.parse()?, + }) + } +} + +impl FromStr for Duration32 { + type Err = ParseIntError; + + fn from_str(s: &str) -> Result { + Ok(Duration32 { + seconds: s.parse()?, + }) + } +} + impl ZcashSerialize for DateTime32 { fn zcash_serialize(&self, mut writer: W) -> Result<(), std::io::Error> { writer.write_u32::(self.timestamp) diff --git a/zebra-rpc/src/methods/get_block_template_rpcs/types/get_block_template.rs b/zebra-rpc/src/methods/get_block_template_rpcs/types/get_block_template.rs index f39c4bb20da..8672ac1a13c 100644 --- a/zebra-rpc/src/methods/get_block_template_rpcs/types/get_block_template.rs +++ b/zebra-rpc/src/methods/get_block_template_rpcs/types/get_block_template.rs @@ -30,8 +30,8 @@ use crate::methods::{ pub mod parameters; pub mod proposal; -pub use parameters::*; -pub use proposal::*; +pub use parameters::{GetBlockTemplateCapability, GetBlockTemplateRequestMode, JsonParameters}; +pub use proposal::{proposal_block_from_template, ProposalRejectReason, ProposalResponse}; /// A serialized `getblocktemplate` RPC response in template mode. #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] @@ -286,7 +286,7 @@ impl GetBlockTemplate { } } -#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(untagged)] /// A `getblocktemplate` RPC response. pub enum Response { diff --git a/zebra-rpc/src/methods/get_block_template_rpcs/types/get_block_template/proposal.rs b/zebra-rpc/src/methods/get_block_template_rpcs/types/get_block_template/proposal.rs index 5434390c648..8a44f179ca0 100644 --- a/zebra-rpc/src/methods/get_block_template_rpcs/types/get_block_template/proposal.rs +++ b/zebra-rpc/src/methods/get_block_template_rpcs/types/get_block_template/proposal.rs @@ -1,11 +1,27 @@ +//! getblocktemplate proposal mode implementation. +//! //! `ProposalResponse` is the output of the `getblocktemplate` RPC method in 'proposal' mode. -use super::{GetBlockTemplate, Response}; +use std::{num::ParseIntError, str::FromStr, sync::Arc}; + +use zebra_chain::{ + block::{self, Block, Height}, + serialization::{DateTime32, SerializationError, ZcashDeserializeInto}, + work::equihash::Solution, +}; + +use crate::methods::{ + get_block_template_rpcs::types::{ + default_roots::DefaultRoots, + get_block_template::{GetBlockTemplate, Response}, + }, + GetBlockHash, +}; /// Error response to a `getblocktemplate` RPC request in proposal mode. /// /// See -#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "kebab-case")] pub enum ProposalRejectReason { /// Block proposal rejected as invalid. @@ -15,7 +31,7 @@ pub enum ProposalRejectReason { /// Response to a `getblocktemplate` RPC request in proposal mode. /// /// See -#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(untagged, rename_all = "kebab-case")] pub enum ProposalResponse { /// Block proposal was rejected as invalid, returns `reject-reason` and server `capabilities`. @@ -57,3 +73,140 @@ impl From for Response { Self::TemplateMode(Box::new(template)) } } + +/// The source of the time in the block proposal header. +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub enum TimeSource { + /// The `curtime` field in the template. + /// This is the default time source. + #[default] + CurTime, + + /// The `mintime` field in the template. + MinTime, + + /// The `maxtime` field in the template. + MaxTime, + + /// The supplied time, clamped within the template's `[mintime, maxtime]`. + Clamped(DateTime32), + + /// The current local clock time, clamped within the template's `[mintime, maxtime]`. + ClampedNow, + + /// The raw supplied time, ignoring the `mintime` and `maxtime` in the template. + /// + /// Warning: this can create an invalid block proposal. + Raw(DateTime32), + + /// The raw current local time, ignoring the `mintime` and `maxtime` in the template. + /// + /// Warning: this can create an invalid block proposal. + RawNow, +} + +impl TimeSource { + /// Returns the time from `template` using this time source. + pub fn time_from_template(&self, template: &GetBlockTemplate) -> DateTime32 { + use TimeSource::*; + + match self { + CurTime => template.cur_time, + MinTime => template.min_time, + MaxTime => template.max_time, + Clamped(time) => (*time).clamp(template.min_time, template.max_time), + ClampedNow => DateTime32::now().clamp(template.min_time, template.max_time), + Raw(time) => *time, + RawNow => DateTime32::now(), + } + } + + /// Returns true if this time source uses `max_time` in any way, including clamping. + pub fn uses_max_time(&self) -> bool { + use TimeSource::*; + + match self { + CurTime | MinTime => false, + MaxTime | Clamped(_) | ClampedNow => true, + Raw(_) | RawNow => false, + } + } +} + +impl FromStr for TimeSource { + type Err = ParseIntError; + + fn from_str(s: &str) -> Result { + use TimeSource::*; + + match s.to_lowercase().as_str() { + "curtime" => Ok(CurTime), + "mintime" => Ok(MinTime), + "maxtime" => Ok(MaxTime), + "clampednow" => Ok(ClampedNow), + "rawnow" => Ok(RawNow), + s => match s.strip_prefix("raw") { + // "raw"u32 + Some(raw_value) => Ok(Raw(raw_value.parse()?)), + // "clamped"u32 or just u32 + // this is the default if the argument is just a number + None => Ok(Clamped(s.strip_prefix("clamped").unwrap_or(s).parse()?)), + }, + } + } +} + +/// Returns a block proposal generated from a [`GetBlockTemplate`] RPC response. +/// +/// If `time_source` is not supplied, uses the current time from the template. +pub fn proposal_block_from_template( + template: GetBlockTemplate, + time_source: impl Into>, +) -> Result { + let GetBlockTemplate { + version, + height, + previous_block_hash: GetBlockHash(previous_block_hash), + default_roots: + DefaultRoots { + merkle_root, + block_commitments_hash, + .. + }, + bits: difficulty_threshold, + ref coinbase_txn, + transactions: ref tx_templates, + .. + } = template; + + if Height(height) > Height::MAX { + Err(SerializationError::Parse( + "height field must be lower than Height::MAX", + ))?; + }; + + let time = time_source + .into() + .unwrap_or_default() + .time_from_template(&template); + + let mut transactions = vec![coinbase_txn.data.as_ref().zcash_deserialize_into()?]; + + for tx_template in tx_templates { + transactions.push(tx_template.data.as_ref().zcash_deserialize_into()?); + } + + Ok(Block { + header: Arc::new(block::Header { + version, + previous_block_hash, + merkle_root, + commitment_bytes: block_commitments_hash.bytes_in_serialized_order().into(), + time: time.into(), + difficulty_threshold, + nonce: [0; 32].into(), + solution: Solution::for_proposal(), + }), + transactions, + }) +} diff --git a/zebra-utils/Cargo.toml b/zebra-utils/Cargo.toml index 24379486deb..ff4065683b1 100644 --- a/zebra-utils/Cargo.toml +++ b/zebra-utils/Cargo.toml @@ -4,9 +4,30 @@ authors = ["Zcash Foundation "] license = "MIT OR Apache-2.0" version = "1.0.0-beta.19" edition = "2021" + # Prevent accidental publication of this utility crate. publish = false +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[[bin]] +name = "block-template-to-proposal" +# this setting is required for Zebra's Docker build caches +path = "src/bin/block-template-to-proposal/main.rs" +required-features = ["getblocktemplate-rpcs"] + +[features] +default = [] + +# Production features that activate extra dependencies, or extra features in dependencies + +# Experimental mining RPC support +getblocktemplate-rpcs = [ + "zebra-rpc/getblocktemplate-rpcs", + "zebra-node-services/getblocktemplate-rpcs", + "zebra-chain/getblocktemplate-rpcs", +] + [dependencies] color-eyre = "0.6.2" # This is a transitive dependency via color-eyre. @@ -21,3 +42,6 @@ tracing-subscriber = "0.3.16" zebra-node-services = { path = "../zebra-node-services" } zebra-chain = { path = "../zebra-chain" } + +# Experimental feature getblocktemplate-rpcs +zebra-rpc = { path = "../zebra-rpc", optional = true } diff --git a/zebra-utils/src/bin/block-template-to-proposal/args.rs b/zebra-utils/src/bin/block-template-to-proposal/args.rs new file mode 100644 index 00000000000..3378a784a5d --- /dev/null +++ b/zebra-utils/src/bin/block-template-to-proposal/args.rs @@ -0,0 +1,25 @@ +//! block-template-to-proposal arguments +//! +//! For usage please refer to the program help: `block-template-to-proposal --help` + +use structopt::StructOpt; + +use zebra_rpc::methods::get_block_template_rpcs::get_block_template::proposal::TimeSource; + +/// block-template-to-proposal arguments +#[derive(Clone, Debug, Eq, PartialEq, StructOpt)] +pub struct Args { + /// The source of the time in the block proposal header. + /// Format: "curtime", "mintime", "maxtime", ["clamped"]u32, "raw"u32 + /// Clamped times are clamped to the template's [`mintime`, `maxtime`]. + /// Raw times are used unmodified: this can produce invalid proposals. + #[structopt(default_value = "CurTime", short, long)] + pub time_source: TimeSource, + + /// The JSON block template. + /// If this argument is not supplied, the template is read from standard input. + /// + /// The template and proposal structures are printed to stderr. + #[structopt(last = true)] + pub template: Option, +} diff --git a/zebra-utils/src/bin/block-template-to-proposal/main.rs b/zebra-utils/src/bin/block-template-to-proposal/main.rs new file mode 100644 index 00000000000..54c7f6e116b --- /dev/null +++ b/zebra-utils/src/bin/block-template-to-proposal/main.rs @@ -0,0 +1,112 @@ +//! Transforms a JSON block template into a hex-encoded block proposal. +//! +//! Prints the parsed template and parsed proposal structures to stderr. +//! +//! For usage please refer to the program help: `block-template-to-proposal --help` + +use std::io::Read; + +use color_eyre::eyre::Result; +use serde_json::Value; +use structopt::StructOpt; + +use zebra_chain::serialization::{DateTime32, ZcashSerialize}; +use zebra_rpc::methods::get_block_template_rpcs::{ + get_block_template::proposal_block_from_template, + types::{get_block_template::GetBlockTemplate, long_poll::LONG_POLL_ID_LENGTH}, +}; +use zebra_utils::init_tracing; + +mod args; + +/// The minimum number of characters in a valid `getblocktemplate JSON response. +/// +/// The fields we use take up around ~800 bytes. +const MIN_TEMPLATE_BYTES: usize = 500; + +/// Process entry point for `block-template-to-proposal` +#[allow(clippy::print_stdout, clippy::print_stderr)] +fn main() -> Result<()> { + // initialise + init_tracing(); + color_eyre::install()?; + + // get arguments from command-line or stdin + let args = args::Args::from_args(); + + let time_source = args.time_source; + + // Get template from command-line or standard input + let template = args.template.unwrap_or_else(|| { + let mut template = String::new(); + let bytes_read = std::io::stdin().read_to_string(&mut template).expect("missing JSON block template: must be supplied on command-line or standard input"); + + if bytes_read < MIN_TEMPLATE_BYTES { + panic!("JSON block template is too small: expected at least {MIN_TEMPLATE_BYTES} characters"); + } + + template + }); + + // parse string to generic json + let mut template: Value = serde_json::from_str(&template)?; + eprintln!( + "{}", + serde_json::to_string_pretty(&template).expect("re-serialization never fails") + ); + + let template_obj = template + .as_object_mut() + .expect("template must be a JSON object"); + + // replace zcashd keys that are incompatible with Zebra + + // the longpollid key is in a node-specific format, but this tool doesn't use it, + // so we can replace it with a dummy value + template_obj["longpollid"] = "0".repeat(LONG_POLL_ID_LENGTH).into(); + + // provide dummy keys that Zebra requires but zcashd does not always have + + // the transaction.*.required keys are not used by this tool, + // so we can use any value here + template_obj["coinbasetxn"]["required"] = true.into(); + for tx in template_obj["transactions"] + .as_array_mut() + .expect("transactions must be a JSON array") + { + tx["required"] = false.into(); + } + + // the maxtime field is used by this tool + // if it is missing, substitute a valid value + let current_time: DateTime32 = template_obj["curtime"] + .to_string() + .parse() + .expect("curtime is always a valid DateTime32"); + + template_obj.entry("maxtime").or_insert_with(|| { + if time_source.uses_max_time() { + eprintln!( + "maxtime field is missing, using curtime for maxtime: {:?}", + current_time, + ); + } + + current_time.timestamp().into() + }); + + // parse the modified json to template type + let template: GetBlockTemplate = serde_json::from_value(template)?; + + // generate proposal according to arguments + let proposal = proposal_block_from_template(template, time_source)?; + eprintln!("{proposal:#?}"); + + let proposal = proposal + .zcash_serialize_to_vec() + .expect("serialization to Vec never fails"); + + println!("{}", hex::encode(proposal)); + + Ok(()) +} diff --git a/zebra-utils/zcash-rpc-block-template-to-proposal b/zebra-utils/zcash-rpc-block-template-to-proposal new file mode 100755 index 00000000000..2c5e8a08b39 --- /dev/null +++ b/zebra-utils/zcash-rpc-block-template-to-proposal @@ -0,0 +1,227 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Gets a block template from a Zcash node instance, +# turns it into a block proposal using `block-template-to-proposal`, +# and sends it to one or more Zcash node instances (which can include the same node). +# +# If there are multiple proposal ports, displays a diff of the responses. +# +# Uses `zcash-cli` with the RPC ports supplied on the command-line. + +function usage() +{ + echo "Usage:" + echo "$0 block-template-rpc-port proposal-rpc-port [extra-proposal-rpc-port...] -- [extra-block-template-rpc-json-fields] [extra-proposal-rpc-fields]" +} + +# Override the commands used by this script using these environmental variables: +ZCASH_CLI="${ZCASH_CLI:-zcash-cli}" +DIFF="${DIFF:-diff --unified --color=always}" +BLOCK_TEMPLATE_TO_PROPOSAL="${BLOCK_TEMPLATE_TO_PROPOSAL:-block-template-to-proposal}" +# time how long a command takes to run +TIME="time" +# display the current date and time +DATE="date --rfc-3339=seconds" + +# Override the settings for this script using these environmental variables: +TIME_SOURCES="${TIME_SOURCES:-CurTime MinTime MaxTime ClampedNow}" + +# Process arguments + +if [ $# -lt 2 ]; then + usage + exit 1 +fi + +TEMPLATE_RPC_PORT=$1 +shift + +PROPOSAL_RPC_PORTS="" +while [ -n "${1:-}" ] && [ "${1-}" != "--" ]; do + PROPOSAL_RPC_PORTS="$PROPOSAL_RPC_PORTS $1" + shift +done + +if [ "${1-}" == "--" ]; then + shift +fi + +TEMPLATE_ARG="" +if [ $# -ge 1 ]; then + TEMPLATE_ARG="${1:-}" + shift +fi +TEMPLATE_ARG_FULL="{ \"mode\": \"template\" ${TEMPLATE_ARG:+, $TEMPLATE_ARG} }" + +PROPOSAL_ARG="" +if [ $# -ge 1 ]; then + PROPOSAL_ARG="${1:-}" + shift +fi +PROPOSAL_ARG_NO_DATA="{ \"mode\": \"proposal\", \"data\": \"...\" ${PROPOSAL_ARG:+, $PROPOSAL_ARG} }" + +if [ $# -ge 1 ]; then + usage + exit 1 +fi + +$DATE + +# Use an easily identified temp directory name, +# but fall back to the default temp name if `mktemp` does not understand `--suffix`. +ZCASH_RPC_TMP_DIR=$(mktemp --suffix=.block-template-proposal -d 2>/dev/null || mktemp -d) + +TEMPLATE_NODE_RELEASE_INFO="$ZCASH_RPC_TMP_DIR/template-check-getinfo.json" +PROPOSAL_NODES_RELEASE_INFO_BASE="$ZCASH_RPC_TMP_DIR/proposal-check-getinfo" + +echo "Checking getblocktemplate node release info..." +$ZCASH_CLI -rpcport="$TEMPLATE_RPC_PORT" getinfo > "$TEMPLATE_NODE_RELEASE_INFO" + +TEMPLATE_NODE=$(cat "$TEMPLATE_NODE_RELEASE_INFO" | grep '"subversion"' | \ + cut -d: -f2 | cut -d/ -f2 | \ + tr 'A-Z' 'a-z' | sed 's/magicbean/zcashd/ ; s/zebra$/zebrad/') + +echo "Connected to $TEMPLATE_NODE (port $TEMPLATE_RPC_PORT) for getblocktemplate $TEMPLATE_ARG_FULL" + +echo + +echo "Checking proposal nodes release info..." + +for PORT in $PROPOSAL_RPC_PORTS; do + PROPOSAL_NODE_RELEASE_INFO=$PROPOSAL_NODES_RELEASE_INFO_BASE.$PORT.json + + $ZCASH_CLI -rpcport="$PORT" getinfo > "$PROPOSAL_NODE_RELEASE_INFO" + + PROPOSAL_NODE=$(cat "$PROPOSAL_NODE_RELEASE_INFO" | grep '"subversion"' | \ + cut -d: -f2 | cut -d/ -f2 | \ + tr 'A-Z' 'a-z' | sed 's/magicbean/zcashd/ ; s/zebra$/zebrad/') + + echo "Connected to $PROPOSAL_NODE (port $PORT) for getblocktemplate $PROPOSAL_ARG_NO_DATA" +done + +echo + +TEMPLATE_NODE_BLOCKCHAIN_INFO="$ZCASH_RPC_TMP_DIR/template-check-getblockchaininfo.json" +PROPOSAL_NODES_BLOCKCHAIN_INFO_BASE="$ZCASH_RPC_TMP_DIR/proposal-check-getblockchaininfo" + +echo "Checking $TEMPLATE_NODE template network and tip height..." +$ZCASH_CLI -rpcport="$TEMPLATE_RPC_PORT" getblockchaininfo > "$TEMPLATE_NODE_BLOCKCHAIN_INFO" + +TEMPLATE_NET=$(cat "$TEMPLATE_NODE_BLOCKCHAIN_INFO" | grep '"chain"' | cut -d: -f2 | tr -d ' ,"') +TEMPLATE_HEIGHT=$(cat "$TEMPLATE_NODE_BLOCKCHAIN_INFO" | grep '"blocks"' | cut -d: -f2 | tr -d ' ,"') + +echo "Checking proposal nodes network and tip height..." + +for PORT in $PROPOSAL_RPC_PORTS; do + PROPOSAL_NODE_BLOCKCHAIN_INFO=$PROPOSAL_NODES_BLOCKCHAIN_INFO_BASE.$PORT.json + + $ZCASH_CLI -rpcport="$PORT" getblockchaininfo > "$PROPOSAL_NODE_BLOCKCHAIN_INFO" + + PROPOSAL_NET=$(cat "$PROPOSAL_NODE_BLOCKCHAIN_INFO" | grep '"chain"' | cut -d: -f2 | tr -d ' ,"') + PROPOSAL_HEIGHT=$(cat "$PROPOSAL_NODE_BLOCKCHAIN_INFO" | grep '"blocks"' | cut -d: -f2 | tr -d ' ,"') + + if [ "$PROPOSAL_NET" != "$TEMPLATE_NET" ]; then + echo "WARNING: sending block templates between different networks:" + echo "$TEMPLATE_NODE (RPC port $TEMPLATE_RPC_PORT) template is on: $TEMPLATE_NET" + echo "RPC port $PORT proposal is on: $PROPOSAL_NET" + echo + fi + + if [ "$PROPOSAL_HEIGHT" -ne "$TEMPLATE_HEIGHT" ]; then + echo "WARNING: proposing block templates at different heights:" + echo "$TEMPLATE_NODE (RPC port $TEMPLATE_RPC_PORT) template is on: $TEMPLATE_HEIGHT" + echo "RPC port $PORT proposal is on: $PROPOSAL_HEIGHT" + echo + fi +done + +echo + +TEMPLATE_NODE_TEMPLATE_RESPONSE="$ZCASH_RPC_TMP_DIR/template-getblocktemplate-template.json" + +echo "getblocktemplate template request ($TEMPLATE_NODE port $TEMPLATE_RPC_PORT):" +echo "getblocktemplate $TEMPLATE_ARG_FULL" +echo + +echo "Querying $TEMPLATE_NODE $TEMPLATE_NET chain at height >=$TEMPLATE_HEIGHT..." +$DATE +$TIME $ZCASH_CLI -rpcport="$TEMPLATE_RPC_PORT" getblocktemplate "$TEMPLATE_ARG_FULL" > "$TEMPLATE_NODE_TEMPLATE_RESPONSE" + +echo "Block template data is in $TEMPLATE_NODE_TEMPLATE_RESPONSE" +#cat "$TEMPLATE_NODE_TEMPLATE_RESPONSE" + +echo + +PROPOSAL_DATA_BASE="$ZCASH_RPC_TMP_DIR/proposal-data" + +echo "Turning the template into block proposal data using $BLOCK_TEMPLATE_TO_PROPOSAL..." + +for TIME_SOURCE in $TIME_SOURCES; do + PROPOSAL_DATA="$PROPOSAL_DATA_BASE.$TIME_SOURCE.json" + PROPOSAL_DEBUG="$PROPOSAL_DATA_BASE.$TIME_SOURCE.debug" + + echo -n '{ "mode": "proposal", ' > "$PROPOSAL_DATA" + echo -n '"data": "' >> "$PROPOSAL_DATA" + cat "$TEMPLATE_NODE_TEMPLATE_RESPONSE" | \ + $BLOCK_TEMPLATE_TO_PROPOSAL \ + 2> "$PROPOSAL_DEBUG" | \ + (tr -d '\r\n' || true) \ + >> "$PROPOSAL_DATA" + echo -n '"' >> "$PROPOSAL_DATA" + echo -n "${PROPOSAL_ARG:+, $PROPOSAL_ARG} }" >> "$PROPOSAL_DATA" +done + +echo "Block proposal data is in $PROPOSAL_DATA_BASE*" +#cat "$PROPOSAL_DATA_BASE"* + +echo +echo + +echo "getblocktemplate proposal submissions:" +echo "getblocktemplate $PROPOSAL_ARG_NO_DATA" +$DATE +echo + +PROPOSAL_NODES_PROPOSAL_RESPONSE_BASE="$ZCASH_RPC_TMP_DIR/proposal-check-getblocktemplate-proposal" +PROPOSAL_NODES_PROPOSAL_RESPONSE_LIST="" + +for TIME_SOURCE in $TIME_SOURCES; do + PROPOSAL_DATA="$PROPOSAL_DATA_BASE.$TIME_SOURCE.json" + + for PORT in $PROPOSAL_RPC_PORTS; do + PROPOSAL_NODE_PROPOSAL_RESPONSE=$PROPOSAL_NODES_PROPOSAL_RESPONSE_BASE.$TIME_SOURCE.$PORT.json + PROPOSAL_NODES_PROPOSAL_RESPONSE_LIST="${PROPOSAL_NODES_PROPOSAL_RESPONSE_LIST:+$PROPOSAL_NODES_PROPOSAL_RESPONSE_LIST }$PROPOSAL_NODE_PROPOSAL_RESPONSE" + + # read the proposal data from a file, to avoid command-line length limits + cat "$PROPOSAL_DATA" | \ + $TIME $ZCASH_CLI -rpcport="$PORT" -stdin getblocktemplate \ + > "$PROPOSAL_NODE_PROPOSAL_RESPONSE" || \ + echo "$ZCASH_CLI -rpcport=$PORT exited with an error" + done +done + +echo + +echo "Proposal response diffs between ports $PROPOSAL_RPC_PORTS and time sources $TIME_SOURCES:" + +$DIFF --from-file=$PROPOSAL_NODES_PROPOSAL_RESPONSE_LIST && \ + echo "getblocktemplate proposal responses were identical" + +echo + +EXIT_STATUS=0 +for RESPONSE in $PROPOSAL_NODES_PROPOSAL_RESPONSE_LIST; do + if [ -s "$RESPONSE" ]; then + echo "Node said proposal was invalid, error response from $RESPONSE:" + cat "$RESPONSE" + EXIT_STATUS=1 + else + echo "Node said proposal was valid, empty success response in $RESPONSE" + fi +done + +$DATE + +exit $EXIT_STATUS diff --git a/zebrad/tests/common/get_block_template_rpcs/get_block_template.rs b/zebrad/tests/common/get_block_template_rpcs/get_block_template.rs index 93c59670d60..74069655993 100644 --- a/zebrad/tests/common/get_block_template_rpcs/get_block_template.rs +++ b/zebrad/tests/common/get_block_template_rpcs/get_block_template.rs @@ -5,22 +5,14 @@ //! //! After finishing the sync, it will call getblocktemplate. -use std::{sync::Arc, time::Duration}; +use std::time::Duration; use color_eyre::eyre::{eyre, Context, Result}; -use zebra_chain::{ - block::{self, Block, Height}, - parameters::Network, - serialization::{ZcashDeserializeInto, ZcashSerialize}, - work::equihash::Solution, -}; -use zebra_rpc::methods::{ - get_block_template_rpcs::{ - get_block_template::{GetBlockTemplate, ProposalResponse}, - types::default_roots::DefaultRoots, - }, - GetBlockHash, +use zebra_chain::{parameters::Network, serialization::ZcashSerialize}; +use zebra_rpc::methods::get_block_template_rpcs::{ + get_block_template::{proposal::TimeSource, ProposalResponse}, + types::get_block_template::proposal_block_from_template, }; use crate::common::{ @@ -153,7 +145,12 @@ async fn try_validate_block_template(client: &RPCRequestClient) -> Result<()> { // Propose a new block with an empty solution and nonce field tracing::info!("calling getblocktemplate with a block proposal...",); - for proposal_block in proposal_block_from_template(response_json_result)? { + // TODO: update this to use all valid time sources in the next PR + #[allow(clippy::single_element_loop)] + for proposal_block in [proposal_block_from_template( + response_json_result, + TimeSource::CurTime, + )?] { let raw_proposal_block = hex::encode(proposal_block.zcash_serialize_to_vec()?); let json_result = client @@ -181,53 +178,3 @@ async fn try_validate_block_template(client: &RPCRequestClient) -> Result<()> { Ok(()) } - -/// Make block proposals from [`GetBlockTemplate`] -/// -/// Returns an array of 3 block proposals using `curtime`, `mintime`, and `maxtime` -/// for their `block.header.time` fields. -#[allow(dead_code)] -fn proposal_block_from_template( - GetBlockTemplate { - version, - height, - previous_block_hash: GetBlockHash(previous_block_hash), - default_roots: - DefaultRoots { - merkle_root, - block_commitments_hash, - .. - }, - bits: difficulty_threshold, - coinbase_txn, - transactions: tx_templates, - cur_time, - min_time, - max_time, - .. - }: GetBlockTemplate, -) -> Result<[Block; 3]> { - if Height(height) > Height::MAX { - Err(eyre!("height field must be lower than Height::MAX"))?; - }; - - let mut transactions = vec![coinbase_txn.data.as_ref().zcash_deserialize_into()?]; - - for tx_template in tx_templates { - transactions.push(tx_template.data.as_ref().zcash_deserialize_into()?); - } - - Ok([cur_time, min_time, max_time].map(|time| Block { - header: Arc::new(block::Header { - version, - previous_block_hash, - merkle_root, - commitment_bytes: block_commitments_hash.bytes_in_serialized_order().into(), - time: time.into(), - difficulty_threshold, - nonce: [0; 32].into(), - solution: Solution::for_proposal(), - }), - transactions: transactions.clone(), - })) -}