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 1fed70da9e
.
* 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
This commit is contained in:
parent
256b1c0008
commit
6a06cbf3ad
|
@ -5633,6 +5633,7 @@ dependencies = [
|
||||||
"tracing-subscriber 0.3.16",
|
"tracing-subscriber 0.3.16",
|
||||||
"zebra-chain",
|
"zebra-chain",
|
||||||
"zebra-node-services",
|
"zebra-node-services",
|
||||||
|
"zebra-rpc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
//! DateTime types with specific serialization invariants.
|
//! 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 byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||||
use chrono::{TimeZone, Utc};
|
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.
|
/// 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)]
|
#[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<Self, Self::Err> {
|
||||||
|
Ok(DateTime32 {
|
||||||
|
timestamp: s.parse()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Duration32 {
|
||||||
|
type Err = ParseIntError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
Ok(Duration32 {
|
||||||
|
seconds: s.parse()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ZcashSerialize for DateTime32 {
|
impl ZcashSerialize for DateTime32 {
|
||||||
fn zcash_serialize<W: std::io::Write>(&self, mut writer: W) -> Result<(), std::io::Error> {
|
fn zcash_serialize<W: std::io::Write>(&self, mut writer: W) -> Result<(), std::io::Error> {
|
||||||
writer.write_u32::<LittleEndian>(self.timestamp)
|
writer.write_u32::<LittleEndian>(self.timestamp)
|
||||||
|
|
|
@ -30,8 +30,8 @@ use crate::methods::{
|
||||||
pub mod parameters;
|
pub mod parameters;
|
||||||
pub mod proposal;
|
pub mod proposal;
|
||||||
|
|
||||||
pub use parameters::*;
|
pub use parameters::{GetBlockTemplateCapability, GetBlockTemplateRequestMode, JsonParameters};
|
||||||
pub use proposal::*;
|
pub use proposal::{proposal_block_from_template, ProposalRejectReason, ProposalResponse};
|
||||||
|
|
||||||
/// A serialized `getblocktemplate` RPC response in template mode.
|
/// A serialized `getblocktemplate` RPC response in template mode.
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
#[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)]
|
#[serde(untagged)]
|
||||||
/// A `getblocktemplate` RPC response.
|
/// A `getblocktemplate` RPC response.
|
||||||
pub enum Response {
|
pub enum Response {
|
||||||
|
|
|
@ -1,11 +1,27 @@
|
||||||
|
//! getblocktemplate proposal mode implementation.
|
||||||
|
//!
|
||||||
//! `ProposalResponse` is the output of the `getblocktemplate` RPC method in 'proposal' mode.
|
//! `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.
|
/// Error response to a `getblocktemplate` RPC request in proposal mode.
|
||||||
///
|
///
|
||||||
/// See <https://en.bitcoin.it/wiki/BIP_0022#Appendix:_Example_Rejection_Reasons>
|
/// See <https://en.bitcoin.it/wiki/BIP_0022#Appendix:_Example_Rejection_Reasons>
|
||||||
#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub enum ProposalRejectReason {
|
pub enum ProposalRejectReason {
|
||||||
/// Block proposal rejected as invalid.
|
/// Block proposal rejected as invalid.
|
||||||
|
@ -15,7 +31,7 @@ pub enum ProposalRejectReason {
|
||||||
/// Response to a `getblocktemplate` RPC request in proposal mode.
|
/// Response to a `getblocktemplate` RPC request in proposal mode.
|
||||||
///
|
///
|
||||||
/// See <https://en.bitcoin.it/wiki/BIP_0023#Block_Proposal>
|
/// See <https://en.bitcoin.it/wiki/BIP_0023#Block_Proposal>
|
||||||
#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(untagged, rename_all = "kebab-case")]
|
#[serde(untagged, rename_all = "kebab-case")]
|
||||||
pub enum ProposalResponse {
|
pub enum ProposalResponse {
|
||||||
/// Block proposal was rejected as invalid, returns `reject-reason` and server `capabilities`.
|
/// Block proposal was rejected as invalid, returns `reject-reason` and server `capabilities`.
|
||||||
|
@ -57,3 +73,140 @@ impl From<GetBlockTemplate> for Response {
|
||||||
Self::TemplateMode(Box::new(template))
|
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<Self, Self::Err> {
|
||||||
|
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<Option<TimeSource>>,
|
||||||
|
) -> Result<Block, SerializationError> {
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -4,9 +4,30 @@ authors = ["Zcash Foundation <zebra@zfnd.org>"]
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
version = "1.0.0-beta.19"
|
version = "1.0.0-beta.19"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# Prevent accidental publication of this utility crate.
|
# Prevent accidental publication of this utility crate.
|
||||||
publish = false
|
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]
|
[dependencies]
|
||||||
color-eyre = "0.6.2"
|
color-eyre = "0.6.2"
|
||||||
# This is a transitive dependency via color-eyre.
|
# 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-node-services = { path = "../zebra-node-services" }
|
||||||
zebra-chain = { path = "../zebra-chain" }
|
zebra-chain = { path = "../zebra-chain" }
|
||||||
|
|
||||||
|
# Experimental feature getblocktemplate-rpcs
|
||||||
|
zebra-rpc = { path = "../zebra-rpc", optional = true }
|
||||||
|
|
|
@ -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<String>,
|
||||||
|
}
|
|
@ -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(())
|
||||||
|
}
|
|
@ -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
|
|
@ -5,22 +5,14 @@
|
||||||
//!
|
//!
|
||||||
//! After finishing the sync, it will call getblocktemplate.
|
//! 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 color_eyre::eyre::{eyre, Context, Result};
|
||||||
|
|
||||||
use zebra_chain::{
|
use zebra_chain::{parameters::Network, serialization::ZcashSerialize};
|
||||||
block::{self, Block, Height},
|
use zebra_rpc::methods::get_block_template_rpcs::{
|
||||||
parameters::Network,
|
get_block_template::{proposal::TimeSource, ProposalResponse},
|
||||||
serialization::{ZcashDeserializeInto, ZcashSerialize},
|
types::get_block_template::proposal_block_from_template,
|
||||||
work::equihash::Solution,
|
|
||||||
};
|
|
||||||
use zebra_rpc::methods::{
|
|
||||||
get_block_template_rpcs::{
|
|
||||||
get_block_template::{GetBlockTemplate, ProposalResponse},
|
|
||||||
types::default_roots::DefaultRoots,
|
|
||||||
},
|
|
||||||
GetBlockHash,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::common::{
|
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
|
// Propose a new block with an empty solution and nonce field
|
||||||
tracing::info!("calling getblocktemplate with a block proposal...",);
|
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 raw_proposal_block = hex::encode(proposal_block.zcash_serialize_to_vec()?);
|
||||||
|
|
||||||
let json_result = client
|
let json_result = client
|
||||||
|
@ -181,53 +178,3 @@ async fn try_validate_block_template(client: &RPCRequestClient) -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
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(),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue