Skip to content

Commit

Permalink
test(rpc): Create a script that submits block proposals to zcashd (#5944
Browse files Browse the repository at this point in the history
)

* Revert "Update code that we're going to delete in the next PR anyway"

This reverts commit 1fed70d.

* 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
  • Loading branch information
teor2345 authored Jan 18, 2023
1 parent 256b1c0 commit 6a06cbf
Show file tree
Hide file tree
Showing 9 changed files with 585 additions and 72 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 26 additions & 2 deletions zebra-chain/src/serialization/date_time.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand Down Expand Up @@ -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 {
fn zcash_serialize<W: std::io::Write>(&self, mut writer: W) -> Result<(), std::io::Error> {
writer.write_u32::<LittleEndian>(self.timestamp)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <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")]
pub enum ProposalRejectReason {
/// Block proposal rejected as invalid.
Expand All @@ -15,7 +31,7 @@ pub enum ProposalRejectReason {
/// Response to a `getblocktemplate` RPC request in proposal mode.
///
/// 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")]
pub enum ProposalResponse {
/// Block proposal was rejected as invalid, returns `reject-reason` and server `capabilities`.
Expand Down Expand Up @@ -57,3 +73,140 @@ impl From<GetBlockTemplate> 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<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,
})
}
24 changes: 24 additions & 0 deletions zebra-utils/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,30 @@ authors = ["Zcash Foundation <[email protected]>"]
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.
Expand All @@ -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 }
25 changes: 25 additions & 0 deletions zebra-utils/src/bin/block-template-to-proposal/args.rs
Original file line number Diff line number Diff line change
@@ -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>,
}
Loading

0 comments on commit 6a06cbf

Please sign in to comment.