Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(mine): Add an internal Zcash miner to Zebra #8136

Merged
merged 56 commits into from
Jan 11, 2024
Merged
Show file tree
Hide file tree
Changes from 54 commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
2594a8e
Patch equihash to use the solver branch
teor2345 Jan 7, 2024
f2b5d9c
Add an internal-miner feature and set up its dependencies
teor2345 Jan 7, 2024
de5091d
Remove 'Experimental' from mining RPC docs
teor2345 Jan 7, 2024
1cdb65b
Fix a nightly clippy::question_mark lint
teor2345 Jan 7, 2024
5e42efe
Move a byte array utility function to zebra-chain
teor2345 Jan 7, 2024
9de4fc3
fixup! Add an internal-miner feature and set up its dependencies
teor2345 Jan 7, 2024
4b2bc08
Add an equihash::Solution::solve() method with difficulty checks
teor2345 Jan 8, 2024
6cbe8ec
Check solution is valid before returning it
teor2345 Jan 8, 2024
2ddc058
Add a TODO to check for peers before mining
teor2345 Jan 8, 2024
a2a49b5
Move config validation into GetBlockTemplateRpcImpl::new()
teor2345 Jan 8, 2024
fe2b65f
fixup! fixup! Add an internal-miner feature and set up its dependencies
teor2345 Jan 8, 2024
92f4783
Use the same generic constraints for GetBlockTemplateRpcImpl struct a…
teor2345 Jan 8, 2024
76de9fe
Start adding an internal miner component
teor2345 Jan 8, 2024
ba5cd31
Add the miner task to the start command
teor2345 Jan 8, 2024
ba00c28
Add basic miner code
teor2345 Jan 8, 2024
2543aff
Split out a method to mine one block
teor2345 Jan 8, 2024
d73fccc
Spawn to a blocking thread
teor2345 Jan 8, 2024
62e7670
Wait until a valid template is available
teor2345 Jan 8, 2024
a3d45fe
Handle shutdown
teor2345 Jan 8, 2024
d9849fc
Run mining on low priority threads
teor2345 Jan 8, 2024
b513113
Ignore some invalid solutions
teor2345 Jan 8, 2024
9e3ed36
Use a difference nonce for each solver thread
teor2345 Jan 8, 2024
37ca967
Update TODOs
teor2345 Jan 8, 2024
d3c81a1
Change the patch into a renamed dependency to simplify crate releases
teor2345 Jan 8, 2024
384d892
Clean up instrumentation and TODOs
teor2345 Jan 8, 2024
be5fce5
Make RPC instances cloneable and clean up generics
teor2345 Jan 9, 2024
96d57a7
Make LongPollId Copy so it's easier to use
teor2345 Jan 9, 2024
b8bde14
Add API to restart mining if there's a new block template
teor2345 Jan 9, 2024
1c7ad51
Actually restart mining if there's a new block template
teor2345 Jan 9, 2024
43349d6
Tidy instrumentation
teor2345 Jan 9, 2024
a92a3fa
fixup! Move config validation into GetBlockTemplateRpcImpl::new()
teor2345 Jan 9, 2024
24079ce
fixup! Make RPC instances cloneable and clean up generics
teor2345 Jan 9, 2024
669aead
Run the template generator and one miner concurrently
teor2345 Jan 9, 2024
29596f9
Reduce logging
teor2345 Jan 9, 2024
dc39853
Fix a bug in getblocktemplate RPC tip change detection
teor2345 Jan 9, 2024
9657302
Work around some watch channel change bugs
teor2345 Jan 9, 2024
4edc15e
Rate-limit template changes in the receiver
teor2345 Jan 9, 2024
b312ebb
Run one mining solver per available core
teor2345 Jan 10, 2024
29ea11f
Use updated C code with double-free protection
teor2345 Jan 10, 2024
ad022c3
Update to the latest solver branch
teor2345 Jan 10, 2024
bccf803
Return and submit all valid solutions
teor2345 Jan 10, 2024
6e6bc01
Document what INPUT_LENGTH means
teor2345 Jan 10, 2024
8656076
Fix watch channel change detection
teor2345 Jan 10, 2024
ced8107
Don't return early when a mining task fails
teor2345 Jan 10, 2024
5bcc618
Spawn async miner tasks to avoid cooperative blocking, deadlocks, and…
teor2345 Jan 10, 2024
630b72c
Merge branch 'main' into internal-mine
teor2345 Jan 10, 2024
dad12ad
Make existing parallelism docs and configs consistent
teor2345 Jan 10, 2024
54d27cb
Add a mining parallelism config
teor2345 Jan 10, 2024
cc1e737
Use the minimum of the configured or available threads for mining
teor2345 Jan 10, 2024
c70438a
Ignore optional feature fields in tests
teor2345 Jan 10, 2024
1c6802f
Downgrade some frequent logs to debug
teor2345 Jan 10, 2024
df6bac4
Document new zebrad features and tasks
teor2345 Jan 10, 2024
fa850f5
Describe the internal-miner feature in the CHANGELOG
teor2345 Jan 10, 2024
1b180e1
Update dependency to de-duplicate equihash solutions
teor2345 Jan 10, 2024
57bf84d
Use futures::StreamExt instead of TryStreamExt
teor2345 Jan 10, 2024
44298fb
Fix a panic message typo
teor2345 Jan 10, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,20 @@ All notable changes to Zebra are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org).

## [Zebra 1.6.0](https://github.com/ZcashFoundation/zebra/releases/tag/v1.6.0) - TODO: 2024-01-??

This release:
- TODO: summary of other important changes
- adds an experimental `internal-miner` feature, which mines blocks within `zebrad`. This feature
is only supported on testnet. Use a more efficient GPU or ASIC for mainnet mining.

TODO: the rest of the changelog


## [Zebra 1.5.0](https://github.com/ZcashFoundation/zebra/releases/tag/v1.5.0) - 2023-11-28

This release:
- fixes a panic that was introduced in Zebra v1.4.0, which happens in rare circumstances when reading cached sprout or history trees.
- fixes a panic that was introduced in Zebra v1.4.0, which happens in rare circumstances when reading cached sprout or history trees.
- further improves how Zebra recovers from network interruptions and prevents potential network hangs.
- limits the ability of synthetic nodes to spread throughout the network through Zebra to address some of the Ziggurat red team report.

Expand Down
31 changes: 28 additions & 3 deletions Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1346,6 +1346,16 @@ dependencies = [
"byteorder",
]

[[package]]
name = "equihash"
version = "0.2.0"
source = "git+https://github.com/ZcashFoundation/librustzcash.git?branch=equihash-solver-tromp#251098313920466958fcd05b25e151d4edd3a1b1"
dependencies = [
"blake2b_simd",
"byteorder",
"cc",
]

[[package]]
name = "equivalent"
version = "1.0.1"
Expand Down Expand Up @@ -4353,6 +4363,20 @@ dependencies = [
"syn 2.0.40",
]

[[package]]
name = "thread-priority"
version = "0.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b72cb4958060ee2d9540cef68bb3871fd1e547037772c7fe7650d5d1cbec53b3"
dependencies = [
"bitflags 1.3.2",
"cfg-if 1.0.0",
"libc",
"log",
"rustversion",
"winapi",
]

[[package]]
name = "thread_local"
version = "1.1.7"
Expand Down Expand Up @@ -5619,7 +5643,7 @@ dependencies = [
"blake2s_simd",
"bls12_381",
"byteorder",
"equihash",
"equihash 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"ff",
"fpe",
"group",
Expand Down Expand Up @@ -5717,7 +5741,8 @@ dependencies = [
"criterion",
"displaydoc",
"ed25519-zebra",
"equihash",
"equihash 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"equihash 0.2.0 (git+https://github.com/ZcashFoundation/librustzcash.git?branch=equihash-solver-tromp)",
"futures",
"group",
"halo2_proofs",
Expand Down Expand Up @@ -5873,7 +5898,6 @@ dependencies = [
"jsonrpc-core",
"jsonrpc-derive",
"jsonrpc-http-server",
"num_cpus",
"proptest",
"rand 0.8.5",
"serde",
Expand Down Expand Up @@ -6072,6 +6096,7 @@ dependencies = [
"serde_json",
"tempfile",
"thiserror",
"thread-priority",
"tinyvec",
"tokio",
"tokio-stream",
Expand Down
6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ opt-level = 3
[profile.dev.package.bls12_381]
opt-level = 3

[profile.dev.package.byteorder]
opt-level = 3

[profile.dev.package.equihash]
opt-level = 3

[profile.dev.package.zcash_proofs]
opt-level = 3

Expand Down
9 changes: 9 additions & 0 deletions deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ skip-tree = [
# wait for hdwallet to upgrade
{ name = "ring", version = "=0.16.20" },

# wait for the equihash/solver feature to merge
# https://github.com/zcash/librustzcash/pull/1083
# https://github.com/zcash/librustzcash/pull/1088
{ name = "equihash", version = "=0.2.0" },

# zebra-utils dependencies

# wait for structopt upgrade (or upgrade to clap 4)
Expand Down Expand Up @@ -137,6 +142,10 @@ unknown-git = "deny"
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
# List of URLs for allowed Git repositories
allow-git = [
# TODO: remove this after the equihash solver branch is merged and released.
#
# "cargo deny" will log a warning in builds without the internal-miner feature. That's ok.
"https://github.com/ZcashFoundation/librustzcash.git"
]

[sources.allow-org]
Expand Down
24 changes: 23 additions & 1 deletion zebra-chain/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,19 @@ async-error = [
"tokio",
]

# Experimental mining RPC support
# Mining RPC support
getblocktemplate-rpcs = [
"zcash_address",
]

# Experimental internal miner support
internal-miner = [
# TODO: replace with "equihash/solver" when that feature is merged and released:
# https://github.com/zcash/librustzcash/pull/1083
# https://github.com/zcash/librustzcash/pull/1088
"equihash-solver",
]

# Experimental elasticsearch support
elasticsearch = []

Expand Down Expand Up @@ -61,7 +69,21 @@ blake2s_simd = "1.0.2"
bridgetree = "0.4.0"
bs58 = { version = "0.5.0", features = ["check"] }
byteorder = "1.5.0"

equihash = "0.2.0"
# Experimental internal miner support
#
# TODO: remove "equihash-solver" when the "equihash/solver" feature is merged and released:
# https://github.com/zcash/librustzcash/pull/1083
# https://github.com/zcash/librustzcash/pull/1088
#
# Use the solver PR:
# - latest: branch = "equihash-solver-tromp",
# - crashing with double-frees: rev = "da26c34772f4922eb13b4a1e7d88a969bbcf6a91",
equihash-solver = { version = "0.2.0", git = "https://github.com/ZcashFoundation/librustzcash.git", branch = "equihash-solver-tromp", features = ["solver"], package = "equihash", optional = true }
# or during development, use the locally checked out and modified version of equihash:
#equihash-solver = { version = "0.2.0", path = "../../librustzcash/components/equihash", features = ["solver"], package = "equihash", optional = true }

group = "0.13.0"
incrementalmerkletree = "0.5.0"
jubjub = "0.10.0"
Expand Down
5 changes: 5 additions & 0 deletions zebra-chain/src/block/header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,11 @@ impl Header {
))?
}
}

/// Compute the hash of this header.
pub fn hash(&self) -> Hash {
Hash::from(self)
}
}

/// A header with a count of the number of transactions in its block.
Expand Down
2 changes: 2 additions & 0 deletions zebra-chain/src/primitives.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ mod address;
#[cfg(feature = "getblocktemplate-rpcs")]
pub use address::Address;

pub mod byte_array;

pub use ed25519_zebra as ed25519;
pub use reddsa;
pub use redjubjub;
Expand Down
14 changes: 14 additions & 0 deletions zebra-chain/src/primitives/byte_array.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//! Functions for modifying byte arrays.

/// Increments `byte_array` by 1, interpreting it as a big-endian integer.
/// If the big-endian integer overflowed, sets all the bytes to zero, and returns `true`.
pub fn increment_big_endian(byte_array: &mut [u8]) -> bool {
// Increment the last byte in the array that is less than u8::MAX, and clear any bytes after it
// to increment the next value in big-endian (lexicographic) order.
let is_wrapped_overflow = byte_array.iter_mut().rev().all(|v| {
*v = v.wrapping_add(1);
v == &0
});

is_wrapped_overflow
}
147 changes: 132 additions & 15 deletions zebra-chain/src/work/equihash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,24 @@ use crate::{
},
};

/// The error type for Equihash
#[cfg(feature = "internal-miner")]
use crate::serialization::AtLeastOne;

/// The error type for Equihash validation.
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
#[error("invalid equihash solution for BlockHeader")]
pub struct Error(#[from] equihash::Error);

/// The error type for Equihash solving.
#[derive(Copy, Clone, Debug, Eq, PartialEq, thiserror::Error)]
#[error("solver was cancelled")]
pub struct SolverCancelled;

/// The size of an Equihash solution in bytes (always 1344).
pub(crate) const SOLUTION_SIZE: usize = 1344;

/// Equihash Solution.
/// Equihash Solution in compressed format.
///
/// A wrapper around [u8; 1344] because Rust doesn't implement common
/// traits like `Debug`, `Clone`, etc for collections like array
Expand Down Expand Up @@ -53,18 +61,138 @@ impl Solution {
.zcash_serialize(&mut input)
.expect("serialization into a vec can't fail");

// The part of the header before the nonce and solution.
// This data is kept constant during solver runs, so the verifier API takes it separately.
let input = &input[0..Solution::INPUT_LENGTH];

equihash::is_valid_solution(n, k, input, nonce.as_ref(), solution)?;

Ok(())
}

#[cfg(feature = "getblocktemplate-rpcs")]
/// Returns a [`Solution`] containing the bytes from `solution`.
/// Returns an error if `solution` is the wrong length.
pub fn from_bytes(solution: &[u8]) -> Result<Self, SerializationError> {
if solution.len() != SOLUTION_SIZE {
return Err(SerializationError::Parse(
"incorrect equihash solution size",
));
}

let mut bytes = [0; SOLUTION_SIZE];
// Won't panic, because we just checked the length.
bytes.copy_from_slice(solution);

Ok(Self(bytes))
}

/// Returns a [`Solution`] of `[0; SOLUTION_SIZE]` to be used in block proposals.
#[cfg(feature = "getblocktemplate-rpcs")]
pub fn for_proposal() -> Self {
Self([0; SOLUTION_SIZE])
}

/// Mines and returns one or more [`Solution`]s based on a template `header`.
arya2 marked this conversation as resolved.
Show resolved Hide resolved
/// The returned header contains a valid `nonce` and `solution`.
///
/// If `cancel_fn()` returns an error, returns early with `Err(SolverCancelled)`.
///
/// The `nonce` in the header template is taken as the starting nonce. If you are running multiple
/// solvers at the same time, start them with different nonces.
/// The `solution` in the header template is ignored.
///
/// This method is CPU and memory-intensive. It uses 144 MB of RAM and one CPU core while running.
/// It can run for minutes or hours if the network difficulty is high.
#[cfg(feature = "internal-miner")]
#[allow(clippy::unwrap_in_result)]
pub fn solve<F>(
mut header: Header,
mut cancel_fn: F,
) -> Result<AtLeastOne<Header>, SolverCancelled>
where
F: FnMut() -> Result<(), SolverCancelled>,
{
use crate::shutdown::is_shutting_down;

let mut input = Vec::new();
header
.zcash_serialize(&mut input)
.expect("serialization into a vec can't fail");
// Take the part of the header before the nonce and solution.
// This data is kept constant for this solver run.
let input = &input[0..Solution::INPUT_LENGTH];

while !is_shutting_down() {
// Don't run the solver if we'd just cancel it anyway.
cancel_fn()?;

let solutions = equihash_solver::tromp::solve_200_9_compressed(input, || {
arya2 marked this conversation as resolved.
Show resolved Hide resolved
// Cancel the solver if we have a new template.
if cancel_fn().is_err() {
return None;
}

arya2 marked this conversation as resolved.
Show resolved Hide resolved
// This skips the first nonce, which doesn't matter in practice.
teor2345 marked this conversation as resolved.
Show resolved Hide resolved
Self::next_nonce(&mut header.nonce);
Some(*header.nonce)
});

let mut valid_solutions = Vec::new();

// If we got any solutions, try submitting them, because the new template might just
// contain some extra transactions. Mining extra transactions is optional.
for solution in &solutions {
header.solution = Self::from_bytes(solution)
.expect("unexpected invalid solution: incorrect length");

// TODO: work out why we sometimes get invalid solutions here
teor2345 marked this conversation as resolved.
Show resolved Hide resolved
if let Err(error) = header.solution.check(&header) {
info!(?error, "found invalid solution for header");
continue;
}

if Self::difficulty_is_valid(&header) {
valid_solutions.push(header);
}
}

match valid_solutions.try_into() {
Ok(at_least_one_solution) => return Ok(at_least_one_solution),
Err(_is_empty_error) => debug!(
solutions = ?solutions.len(),
"found valid solutions which did not pass the validity or difficulty checks"
),
}
}

Err(SolverCancelled)
}

/// Modifies `nonce` to be the next integer in big-endian order.
/// Wraps to zero if the next nonce would overflow.
#[cfg(feature = "internal-miner")]
fn next_nonce(nonce: &mut [u8; 32]) {
let _ignore_overflow = crate::primitives::byte_array::increment_big_endian(&mut nonce[..]);
}

/// Returns `true` if the `nonce` and `solution` in `header` meet the difficulty threshold.
///
/// Assumes that the difficulty threshold in the header is valid.
#[cfg(feature = "internal-miner")]
fn difficulty_is_valid(header: &Header) -> bool {
teor2345 marked this conversation as resolved.
Show resolved Hide resolved
// Simplified from zebra_consensus::block::check::difficulty_is_valid().
let difficulty_threshold = header
.difficulty_threshold
.to_expanded()
.expect("unexpected invalid header template: invalid difficulty threshold");

// TODO: avoid calculating this hash multiple times
let hash = header.hash();

// Note: this comparison is a u256 integer comparison, like zcashd and bitcoin. Greater
// values represent *less* work.
hash <= difficulty_threshold
}
}

impl PartialEq<Solution> for Solution {
Expand Down Expand Up @@ -109,17 +237,6 @@ impl ZcashSerialize for Solution {
impl ZcashDeserialize for Solution {
fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
let solution: Vec<u8> = (&mut reader).zcash_deserialize_into()?;

if solution.len() != SOLUTION_SIZE {
return Err(SerializationError::Parse(
"incorrect equihash solution size",
));
}

let mut bytes = [0; SOLUTION_SIZE];
// Won't panic, because we just checked the length.
bytes.copy_from_slice(&solution);

Ok(Self(bytes))
Self::from_bytes(&solution)
}
}
Loading
Loading