change(rpc): Provide and parse a long poll ID, but don't use it yet (#5796)
* Declare support for long polling in the getblocktemplate RPC * Add long polling input and ID structs * Split out an update_checksum() function * Implement LongPollId conversion to and from a string * Use the LongPollId type in the RPC * Add a longpollid field to the getblocktemplate parameters and responses, but don't use it yet * Use multiple RPC threads with the getblocktemplate feature, to enable efficient long polling * Update RPC snapshots * Remove the "longpoll" capability, it's for miners, not nodes * Use the long poll length constant in tests * Update snapshots * Remove the "long polling is not supported" error * Fix minor compilation issues after the merge/rebase * Expand long poll id comments * Rename estimated height to local height, because that's what it actually is * Add an invalid params test and fix the long poll id test * Add modified config for config_tests * Instrument all the config sub-tests * Show the missing config file when the test fails * Fix the generated config file * Allow a clippy lint in tests * Explain conversion from bytes to u32 * Remove a duplicate test case
This commit is contained in:
parent
2041fda7bb
commit
a6e6eb5051
|
@ -55,6 +55,8 @@ pub struct Config {
|
||||||
pub debug_force_finished_sync: bool,
|
pub debug_force_finished_sync: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This impl isn't derivable because it depends on features.
|
||||||
|
#[allow(clippy::derivable_impls)]
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
@ -62,8 +64,13 @@ impl Default for Config {
|
||||||
listen_addr: None,
|
listen_addr: None,
|
||||||
|
|
||||||
// Use a single thread, so we can detect RPC port conflicts.
|
// Use a single thread, so we can detect RPC port conflicts.
|
||||||
|
#[cfg(not(feature = "getblocktemplate-rpcs"))]
|
||||||
parallel_cpu_threads: 1,
|
parallel_cpu_threads: 1,
|
||||||
|
|
||||||
|
// Use multiple threads, because we pause requests during getblocktemplate long polling
|
||||||
|
#[cfg(feature = "getblocktemplate-rpcs")]
|
||||||
|
parallel_cpu_threads: 0,
|
||||||
|
|
||||||
// Debug options are always off by default.
|
// Debug options are always off by default.
|
||||||
debug_force_finished_sync: false,
|
debug_force_finished_sync: false,
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,10 +31,17 @@ use zebra_state::{ReadRequest, ReadResponse};
|
||||||
|
|
||||||
use crate::methods::{
|
use crate::methods::{
|
||||||
best_chain_tip_height,
|
best_chain_tip_height,
|
||||||
get_block_template_rpcs::types::{
|
get_block_template_rpcs::{
|
||||||
|
constants::{
|
||||||
|
DEFAULT_SOLUTION_RATE_WINDOW_SIZE, GET_BLOCK_TEMPLATE_CAPABILITIES_FIELD,
|
||||||
|
GET_BLOCK_TEMPLATE_MUTABLE_FIELD, GET_BLOCK_TEMPLATE_NONCE_RANGE_FIELD,
|
||||||
|
MAX_ESTIMATED_DISTANCE_TO_NETWORK_CHAIN_TIP, NOT_SYNCED_ERROR_CODE,
|
||||||
|
},
|
||||||
|
types::{
|
||||||
default_roots::DefaultRoots, get_block_template::GetBlockTemplate,
|
default_roots::DefaultRoots, get_block_template::GetBlockTemplate,
|
||||||
get_block_template_opts::GetBlockTemplateRequestMode, hex_data::HexData, submit_block,
|
get_block_template_opts::GetBlockTemplateRequestMode, hex_data::HexData,
|
||||||
transaction::TransactionTemplate,
|
long_poll::LongPollInput, submit_block, transaction::TransactionTemplate,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
GetBlockHash, MISSING_BLOCK_ERROR_CODE,
|
GetBlockHash, MISSING_BLOCK_ERROR_CODE,
|
||||||
};
|
};
|
||||||
|
@ -44,25 +51,6 @@ pub mod constants;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
pub mod zip317;
|
pub mod zip317;
|
||||||
|
|
||||||
/// The max estimated distance to the chain tip for the getblocktemplate method.
|
|
||||||
///
|
|
||||||
/// Allows the same clock skew as the Zcash network, which is 100 blocks, based on the standard rule:
|
|
||||||
/// > A full validator MUST NOT accept blocks with nTime more than two hours in the future
|
|
||||||
/// > according to its clock. This is not strictly a consensus rule because it is nondeterministic,
|
|
||||||
/// > and clock time varies between nodes.
|
|
||||||
const MAX_ESTIMATED_DISTANCE_TO_NETWORK_CHAIN_TIP: i32 = 100;
|
|
||||||
|
|
||||||
/// The default window size specifying how many blocks to check when estimating the chain's solution rate.
|
|
||||||
///
|
|
||||||
/// Based on default value in zcashd.
|
|
||||||
const DEFAULT_SOLUTION_RATE_WINDOW_SIZE: usize = 120;
|
|
||||||
|
|
||||||
/// The RPC error code used by `zcashd` for when it's still downloading initial blocks.
|
|
||||||
///
|
|
||||||
/// `s-nomp` mining pool expects error code `-10` when the node is not synced:
|
|
||||||
/// <https://github.com/s-nomp/node-stratum-pool/blob/d86ae73f8ff968d9355bb61aac05e0ebef36ccb5/lib/pool.js#L142>
|
|
||||||
pub const NOT_SYNCED_ERROR_CODE: ErrorCode = ErrorCode::ServerError(-10);
|
|
||||||
|
|
||||||
/// getblocktemplate RPC method signatures.
|
/// getblocktemplate RPC method signatures.
|
||||||
#[rpc(server)]
|
#[rpc(server)]
|
||||||
pub trait GetBlockTemplateRpc {
|
pub trait GetBlockTemplateRpc {
|
||||||
|
@ -355,14 +343,6 @@ where
|
||||||
data: None,
|
data: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if options.longpollid.is_some() {
|
|
||||||
return Err(Error {
|
|
||||||
code: ErrorCode::InvalidParams,
|
|
||||||
message: "long polling is currently unsupported by Zebra".to_string(),
|
|
||||||
data: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let miner_address = miner_address.ok_or_else(|| Error {
|
let miner_address = miner_address.ok_or_else(|| Error {
|
||||||
|
@ -375,7 +355,7 @@ where
|
||||||
|
|
||||||
// The tip estimate may not be the same as the one coming from the state
|
// The tip estimate may not be the same as the one coming from the state
|
||||||
// but this is ok for an estimate
|
// but this is ok for an estimate
|
||||||
let (estimated_distance_to_chain_tip, estimated_tip_height) = latest_chain_tip
|
let (estimated_distance_to_chain_tip, local_tip_height) = latest_chain_tip
|
||||||
.estimate_distance_to_network_chain_tip(network)
|
.estimate_distance_to_network_chain_tip(network)
|
||||||
.ok_or_else(|| Error {
|
.ok_or_else(|| Error {
|
||||||
code: ErrorCode::ServerError(0),
|
code: ErrorCode::ServerError(0),
|
||||||
|
@ -386,7 +366,7 @@ where
|
||||||
if !sync_status.is_close_to_tip() || estimated_distance_to_chain_tip > MAX_ESTIMATED_DISTANCE_TO_NETWORK_CHAIN_TIP {
|
if !sync_status.is_close_to_tip() || estimated_distance_to_chain_tip > MAX_ESTIMATED_DISTANCE_TO_NETWORK_CHAIN_TIP {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
estimated_distance_to_chain_tip,
|
estimated_distance_to_chain_tip,
|
||||||
?estimated_tip_height,
|
?local_tip_height,
|
||||||
"Zebra has not synced to the chain tip"
|
"Zebra has not synced to the chain tip"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -441,13 +421,23 @@ where
|
||||||
&auth_data_root,
|
&auth_data_root,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// TODO: use the entire mempool content via a watch channel,
|
||||||
|
// rather than just the randomly selected transactions
|
||||||
|
let long_poll_id = LongPollInput::new(
|
||||||
|
chain_info.tip_height,
|
||||||
|
chain_info.tip_hash,
|
||||||
|
chain_info.max_time,
|
||||||
|
mempool_txs.iter().map(|tx| tx.transaction.id),
|
||||||
|
).into();
|
||||||
|
|
||||||
// Convert into TransactionTemplates
|
// Convert into TransactionTemplates
|
||||||
let mempool_txs = mempool_txs.iter().map(Into::into).collect();
|
let mempool_txs = mempool_txs.iter().map(Into::into).collect();
|
||||||
|
|
||||||
let mutable: Vec<String> = constants::GET_BLOCK_TEMPLATE_MUTABLE_FIELD.iter().map(ToString::to_string).collect();
|
let capabilities: Vec<String> = GET_BLOCK_TEMPLATE_CAPABILITIES_FIELD.iter().map(ToString::to_string).collect();
|
||||||
|
let mutable: Vec<String> = GET_BLOCK_TEMPLATE_MUTABLE_FIELD.iter().map(ToString::to_string).collect();
|
||||||
|
|
||||||
Ok(GetBlockTemplate {
|
Ok(GetBlockTemplate {
|
||||||
capabilities: Vec::new(),
|
capabilities,
|
||||||
|
|
||||||
version: ZCASH_BLOCK_VERSION,
|
version: ZCASH_BLOCK_VERSION,
|
||||||
|
|
||||||
|
@ -466,6 +456,8 @@ where
|
||||||
|
|
||||||
coinbase_txn: TransactionTemplate::from_coinbase(&coinbase_tx, miner_fee),
|
coinbase_txn: TransactionTemplate::from_coinbase(&coinbase_tx, miner_fee),
|
||||||
|
|
||||||
|
long_poll_id,
|
||||||
|
|
||||||
target: format!(
|
target: format!(
|
||||||
"{}",
|
"{}",
|
||||||
chain_info.expected_difficulty
|
chain_info.expected_difficulty
|
||||||
|
@ -477,7 +469,7 @@ where
|
||||||
|
|
||||||
mutable,
|
mutable,
|
||||||
|
|
||||||
nonce_range: constants::GET_BLOCK_TEMPLATE_NONCE_RANGE_FIELD.to_string(),
|
nonce_range: GET_BLOCK_TEMPLATE_NONCE_RANGE_FIELD.to_string(),
|
||||||
|
|
||||||
sigop_limit: MAX_BLOCK_SIGOPS,
|
sigop_limit: MAX_BLOCK_SIGOPS,
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,37 @@
|
||||||
//! Constant values used in mining rpcs methods.
|
//! Constant values used in mining rpcs methods.
|
||||||
|
|
||||||
/// A range of valid nonces that goes from `u32::MIN` to `u32::MAX` as a string.
|
use jsonrpc_core::ErrorCode;
|
||||||
|
|
||||||
|
/// A range of valid block template nonces, that goes from `u32::MIN` to `u32::MAX` as a string.
|
||||||
pub const GET_BLOCK_TEMPLATE_NONCE_RANGE_FIELD: &str = "00000000ffffffff";
|
pub const GET_BLOCK_TEMPLATE_NONCE_RANGE_FIELD: &str = "00000000ffffffff";
|
||||||
|
|
||||||
/// A hardcoded list of fields that the miner can change from the block.
|
/// A hardcoded list of fields that the miner can change from the block template.
|
||||||
pub const GET_BLOCK_TEMPLATE_MUTABLE_FIELD: &[&str] = &["time", "transactions", "prevblock"];
|
pub const GET_BLOCK_TEMPLATE_MUTABLE_FIELD: &[&str] = &[
|
||||||
|
// Standard mutations, copied from zcashd
|
||||||
|
"time",
|
||||||
|
"transactions",
|
||||||
|
"prevblock",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// A hardcoded list of Zebra's getblocktemplate RPC capabilities.
|
||||||
|
/// Currently empty.
|
||||||
|
pub const GET_BLOCK_TEMPLATE_CAPABILITIES_FIELD: &[&str] = &[];
|
||||||
|
|
||||||
|
/// The max estimated distance to the chain tip for the getblocktemplate method.
|
||||||
|
///
|
||||||
|
/// Allows the same clock skew as the Zcash network, which is 100 blocks, based on the standard rule:
|
||||||
|
/// > A full validator MUST NOT accept blocks with nTime more than two hours in the future
|
||||||
|
/// > according to its clock. This is not strictly a consensus rule because it is nondeterministic,
|
||||||
|
/// > and clock time varies between nodes.
|
||||||
|
pub const MAX_ESTIMATED_DISTANCE_TO_NETWORK_CHAIN_TIP: i32 = 100;
|
||||||
|
|
||||||
|
/// The RPC error code used by `zcashd` for when it's still downloading initial blocks.
|
||||||
|
///
|
||||||
|
/// `s-nomp` mining pool expects error code `-10` when the node is not synced:
|
||||||
|
/// <https://github.com/s-nomp/node-stratum-pool/blob/d86ae73f8ff968d9355bb61aac05e0ebef36ccb5/lib/pool.js#L142>
|
||||||
|
pub const NOT_SYNCED_ERROR_CODE: ErrorCode = ErrorCode::ServerError(-10);
|
||||||
|
|
||||||
|
/// The default window size specifying how many blocks to check when estimating the chain's solution rate.
|
||||||
|
///
|
||||||
|
/// Based on default value in zcashd.
|
||||||
|
pub const DEFAULT_SOLUTION_RATE_WINDOW_SIZE: usize = 120;
|
||||||
|
|
|
@ -5,5 +5,6 @@ pub mod get_block_template;
|
||||||
pub mod get_block_template_opts;
|
pub mod get_block_template_opts;
|
||||||
pub mod get_mining_info;
|
pub mod get_mining_info;
|
||||||
pub mod hex_data;
|
pub mod hex_data;
|
||||||
|
pub mod long_poll;
|
||||||
pub mod submit_block;
|
pub mod submit_block;
|
||||||
pub mod transaction;
|
pub mod transaction;
|
||||||
|
|
|
@ -4,7 +4,7 @@ use zebra_chain::{amount, block::ChainHistoryBlockTxAuthCommitmentHash};
|
||||||
|
|
||||||
use crate::methods::{
|
use crate::methods::{
|
||||||
get_block_template_rpcs::types::{
|
get_block_template_rpcs::types::{
|
||||||
default_roots::DefaultRoots, transaction::TransactionTemplate,
|
default_roots::DefaultRoots, long_poll::LongPollId, transaction::TransactionTemplate,
|
||||||
},
|
},
|
||||||
GetBlockHash,
|
GetBlockHash,
|
||||||
};
|
};
|
||||||
|
@ -67,6 +67,10 @@ pub struct GetBlockTemplate {
|
||||||
#[serde(rename = "coinbasetxn")]
|
#[serde(rename = "coinbasetxn")]
|
||||||
pub coinbase_txn: TransactionTemplate<amount::NegativeOrZero>,
|
pub coinbase_txn: TransactionTemplate<amount::NegativeOrZero>,
|
||||||
|
|
||||||
|
/// An ID that represents the chain tip and mempool contents for this template.
|
||||||
|
#[serde(rename = "longpollid")]
|
||||||
|
pub long_poll_id: LongPollId,
|
||||||
|
|
||||||
/// The expected difficulty for the new block displayed in expanded form.
|
/// The expected difficulty for the new block displayed in expanded form.
|
||||||
// TODO: use ExpandedDifficulty type.
|
// TODO: use ExpandedDifficulty type.
|
||||||
pub target: String,
|
pub target: String,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
//! Parameter types for the `getblocktemplate` RPC.
|
//! Parameter types for the `getblocktemplate` RPC.
|
||||||
|
|
||||||
use super::hex_data::HexData;
|
use super::{hex_data::HexData, long_poll::LongPollId};
|
||||||
|
|
||||||
/// Defines whether the RPC method should generate a block template or attempt to validate a block proposal.
|
/// Defines whether the RPC method should generate a block template or attempt to validate a block proposal.
|
||||||
/// `Proposal` mode is currently unsupported and will return an error.
|
/// `Proposal` mode is currently unsupported and will return an error.
|
||||||
|
@ -82,10 +82,8 @@ pub struct JsonParameters {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub capabilities: Vec<GetBlockTemplateCapability>,
|
pub capabilities: Vec<GetBlockTemplateCapability>,
|
||||||
|
|
||||||
/// An id to wait for, in zcashd this is the tip hash and an internal counter.
|
/// An ID that delays the RPC response until the template changes.
|
||||||
///
|
///
|
||||||
/// If provided, the RPC response is delayed until the mempool or chain tip block changes.
|
/// In Zebra, the ID represents the chain tip, max time, and mempool contents.
|
||||||
///
|
pub longpollid: Option<LongPollId>,
|
||||||
/// Currently unsupported and ignored by Zebra.
|
|
||||||
pub longpollid: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,279 @@
|
||||||
|
//! Long polling support for the `getblocktemplate` RPC.
|
||||||
|
//!
|
||||||
|
//! These implementation details are private, and should not be relied upon by miners.
|
||||||
|
//! They are also different from the `zcashd` implementation of long polling.
|
||||||
|
|
||||||
|
use std::{str::FromStr, sync::Arc};
|
||||||
|
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use zebra_chain::{
|
||||||
|
block::{self, Height},
|
||||||
|
transaction::{self, UnminedTxId},
|
||||||
|
};
|
||||||
|
use zebra_node_services::BoxError;
|
||||||
|
|
||||||
|
/// The length of a serialized [`LongPollId`] string.
|
||||||
|
///
|
||||||
|
/// This is an internal Zebra implementation detail, which does not need to match `zcashd`.
|
||||||
|
pub const LONG_POLL_ID_LENGTH: usize = 46;
|
||||||
|
|
||||||
|
/// The inputs to the long polling check.
|
||||||
|
///
|
||||||
|
/// If these inputs change, Zebra should return a response to any open long polls.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct LongPollInput {
|
||||||
|
// Fields that invalidate old work:
|
||||||
|
//
|
||||||
|
/// The tip height used to generate the template containing this long poll ID.
|
||||||
|
///
|
||||||
|
/// If the tip block height changes, a new template must be provided.
|
||||||
|
/// Old work is no longer valid.
|
||||||
|
///
|
||||||
|
/// The height is technically redundant, but it helps with debugging.
|
||||||
|
/// It also reduces the probability of a missed tip change.
|
||||||
|
pub tip_height: Height,
|
||||||
|
|
||||||
|
/// The tip hash used to generate the template containing this long poll ID.
|
||||||
|
///
|
||||||
|
/// If the tip block changes, a new template must be provided.
|
||||||
|
/// Old work is no longer valid.
|
||||||
|
pub tip_hash: block::Hash,
|
||||||
|
|
||||||
|
/// The max time in the same template as this long poll ID.
|
||||||
|
///
|
||||||
|
/// If the max time is reached, a new template must be provided.
|
||||||
|
/// Old work is no longer valid.
|
||||||
|
///
|
||||||
|
/// Ideally, a new template should be provided at least one target block interval before
|
||||||
|
/// the max time. This avoids wasted work.
|
||||||
|
pub max_time: DateTime<Utc>,
|
||||||
|
|
||||||
|
// Fields that allow old work:
|
||||||
|
//
|
||||||
|
/// The effecting hashes of the transactions in the mempool,
|
||||||
|
/// when the template containing this long poll ID was generated.
|
||||||
|
/// We ignore changes to authorizing data.
|
||||||
|
///
|
||||||
|
/// This might be different from the transactions in the template, due to ZIP-317.
|
||||||
|
///
|
||||||
|
/// If the mempool transactions change, a new template might be provided.
|
||||||
|
/// Old work is still valid.
|
||||||
|
pub mempool_transaction_mined_ids: Arc<[transaction::Hash]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LongPollInput {
|
||||||
|
/// Returns a new [`LongPollInput`], based on the supplied fields.
|
||||||
|
pub fn new(
|
||||||
|
tip_height: Height,
|
||||||
|
tip_hash: block::Hash,
|
||||||
|
max_time: DateTime<Utc>,
|
||||||
|
mempool_tx_ids: impl IntoIterator<Item = UnminedTxId>,
|
||||||
|
) -> Self {
|
||||||
|
let mempool_transaction_mined_ids =
|
||||||
|
mempool_tx_ids.into_iter().map(|id| id.mined_id()).collect();
|
||||||
|
|
||||||
|
LongPollInput {
|
||||||
|
tip_height,
|
||||||
|
tip_hash,
|
||||||
|
max_time,
|
||||||
|
mempool_transaction_mined_ids,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The encoded long poll ID, generated from the [`LongPollInput`].
|
||||||
|
///
|
||||||
|
/// `zcashd` IDs are currently 69 hex/decimal digits long.
|
||||||
|
/// Since Zebra's IDs are only 46 hex/decimal digits, mining pools should be able to handle them.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(try_from = "String", into = "String")]
|
||||||
|
pub struct LongPollId {
|
||||||
|
// Fields that invalidate old work:
|
||||||
|
//
|
||||||
|
/// The tip height used to generate the template containing this long poll ID.
|
||||||
|
///
|
||||||
|
/// If the tip block height changes, a new template must be provided.
|
||||||
|
/// Old work is no longer valid.
|
||||||
|
///
|
||||||
|
/// The height is technically redundant, but it helps with debugging.
|
||||||
|
/// It also reduces the probability of a missed tip change.
|
||||||
|
pub tip_height: u32,
|
||||||
|
|
||||||
|
/// A checksum of the tip hash used to generate the template containing this long poll ID.
|
||||||
|
///
|
||||||
|
/// If the tip block changes, a new template must be provided.
|
||||||
|
/// Old work is no longer valid.
|
||||||
|
/// This checksum is not cryptographically secure.
|
||||||
|
///
|
||||||
|
/// It's ok to do a probabilistic check here,
|
||||||
|
/// so we choose a 1 in 2^32 chance of missing a block change.
|
||||||
|
pub tip_hash_checksum: u32,
|
||||||
|
|
||||||
|
/// The max time in the same template as this long poll ID.
|
||||||
|
///
|
||||||
|
/// If the max time is reached, a new template must be provided.
|
||||||
|
/// Old work is no longer valid.
|
||||||
|
///
|
||||||
|
/// Ideally, a new template should be provided at least one target block interval before
|
||||||
|
/// the max time. This avoids wasted work.
|
||||||
|
///
|
||||||
|
/// Zcash times are limited to 32 bits by the consensus rules.
|
||||||
|
pub max_timestamp: u32,
|
||||||
|
|
||||||
|
// Fields that allow old work:
|
||||||
|
//
|
||||||
|
/// The number of transactions in the mempool when the template containing this long poll ID
|
||||||
|
/// was generated. This might be different from the number of transactions in the template,
|
||||||
|
/// due to ZIP-317.
|
||||||
|
///
|
||||||
|
/// If the number of mempool transactions changes, a new template might be provided.
|
||||||
|
/// Old work is still valid.
|
||||||
|
///
|
||||||
|
/// The number of transactions is limited by the mempool DoS limit.
|
||||||
|
///
|
||||||
|
/// Using the number of transactions makes mempool checksum attacks much harder.
|
||||||
|
/// It also helps with debugging, and reduces the probability of a missed mempool change.
|
||||||
|
pub mempool_transaction_count: u32,
|
||||||
|
|
||||||
|
/// A checksum of the effecting hashes of the transactions in the mempool,
|
||||||
|
/// when the template containing this long poll ID was generated.
|
||||||
|
/// We ignore changes to authorizing data.
|
||||||
|
///
|
||||||
|
/// This might be different from the transactions in the template, due to ZIP-317.
|
||||||
|
///
|
||||||
|
/// If the content of the mempool changes, a new template might be provided.
|
||||||
|
/// Old work is still valid.
|
||||||
|
///
|
||||||
|
/// This checksum is not cryptographically secure.
|
||||||
|
///
|
||||||
|
/// It's ok to do a probabilistic check here,
|
||||||
|
/// so we choose a 1 in 2^32 chance of missing a transaction change.
|
||||||
|
///
|
||||||
|
/// # Security
|
||||||
|
///
|
||||||
|
/// Attackers could use dust transactions to keep the checksum at the same value.
|
||||||
|
/// But this would likely change the number of transactions in the mempool.
|
||||||
|
///
|
||||||
|
/// If an attacker could also keep the number of transactions constant,
|
||||||
|
/// a new template will be generated when the tip hash changes, or the max time is reached.
|
||||||
|
pub mempool_transaction_content_checksum: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<LongPollInput> for LongPollId {
|
||||||
|
/// Lossy conversion from LongPollInput to LongPollId.
|
||||||
|
fn from(input: LongPollInput) -> Self {
|
||||||
|
let mut tip_hash_checksum = 0;
|
||||||
|
update_checksum(&mut tip_hash_checksum, input.tip_hash.0);
|
||||||
|
|
||||||
|
let mut mempool_transaction_content_checksum: u32 = 0;
|
||||||
|
for tx_mined_id in input.mempool_transaction_mined_ids.iter() {
|
||||||
|
update_checksum(&mut mempool_transaction_content_checksum, tx_mined_id.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
tip_height: input.tip_height.0,
|
||||||
|
|
||||||
|
tip_hash_checksum,
|
||||||
|
|
||||||
|
// It's ok to do wrapping conversions here,
|
||||||
|
// because long polling checks are probabilistic.
|
||||||
|
max_timestamp: input.max_time.timestamp() as u32,
|
||||||
|
|
||||||
|
mempool_transaction_count: input.mempool_transaction_mined_ids.len() as u32,
|
||||||
|
|
||||||
|
mempool_transaction_content_checksum,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update `checksum` from `item`, so changes in `item` are likely to also change `checksum`.
|
||||||
|
///
|
||||||
|
/// This checksum is not cryptographically secure.
|
||||||
|
fn update_checksum(checksum: &mut u32, item: [u8; 32]) {
|
||||||
|
for chunk in item.chunks(4) {
|
||||||
|
let chunk = chunk.try_into().expect("chunk is u32 size");
|
||||||
|
|
||||||
|
// The endianness of this conversion doesn't matter,
|
||||||
|
// so we make it efficient on the most common platforms.
|
||||||
|
let chunk = u32::from_le_bytes(chunk);
|
||||||
|
|
||||||
|
// It's ok to use xor here, because long polling checks are probabilistic,
|
||||||
|
// and the height, time, and transaction count fields will detect most changes.
|
||||||
|
//
|
||||||
|
// Without those fields, miners could game the xor-ed block hash,
|
||||||
|
// and hide block changes from other miners, gaining an advantage.
|
||||||
|
// But this would reduce their profit under proof of work,
|
||||||
|
// because the first valid block hash a miner generates will pay
|
||||||
|
// a significant block subsidy.
|
||||||
|
*checksum ^= chunk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToString for LongPollId {
|
||||||
|
/// Exact conversion from LongPollId to a string.
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
let LongPollId {
|
||||||
|
tip_height,
|
||||||
|
tip_hash_checksum,
|
||||||
|
max_timestamp,
|
||||||
|
mempool_transaction_count,
|
||||||
|
mempool_transaction_content_checksum,
|
||||||
|
} = self;
|
||||||
|
|
||||||
|
// We can't do this using `serde`, because it names each field,
|
||||||
|
// but we want a single string containing all the fields.
|
||||||
|
format!(
|
||||||
|
// Height as decimal, padded with zeroes to the width of Height::MAX
|
||||||
|
// Checksums as hex, padded with zeroes to the width of u32::MAX
|
||||||
|
// Timestamp as decimal, padded with zeroes to the width of u32::MAX
|
||||||
|
// Transaction Count as decimal, padded with zeroes to the width of u32::MAX
|
||||||
|
"{tip_height:010}\
|
||||||
|
{tip_hash_checksum:08x}\
|
||||||
|
{max_timestamp:010}\
|
||||||
|
{mempool_transaction_count:010}\
|
||||||
|
{mempool_transaction_content_checksum:08x}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for LongPollId {
|
||||||
|
type Err = BoxError;
|
||||||
|
|
||||||
|
/// Exact conversion from a string to LongPollId.
|
||||||
|
fn from_str(long_poll_id: &str) -> Result<Self, Self::Err> {
|
||||||
|
if long_poll_id.len() != LONG_POLL_ID_LENGTH {
|
||||||
|
return Err(format!(
|
||||||
|
"incorrect long poll id length, must be {LONG_POLL_ID_LENGTH} for Zebra"
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
tip_height: long_poll_id[0..10].parse()?,
|
||||||
|
tip_hash_checksum: u32::from_str_radix(&long_poll_id[10..18], 16)?,
|
||||||
|
max_timestamp: long_poll_id[18..28].parse()?,
|
||||||
|
mempool_transaction_count: long_poll_id[28..38].parse()?,
|
||||||
|
mempool_transaction_content_checksum: u32::from_str_radix(
|
||||||
|
&long_poll_id[38..LONG_POLL_ID_LENGTH],
|
||||||
|
16,
|
||||||
|
)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrappers for serde conversion
|
||||||
|
impl From<LongPollId> for String {
|
||||||
|
fn from(id: LongPollId) -> Self {
|
||||||
|
id.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<String> for LongPollId {
|
||||||
|
type Error = BoxError;
|
||||||
|
|
||||||
|
fn try_from(s: String) -> Result<Self, Self::Error> {
|
||||||
|
s.parse()
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,6 +25,7 @@ expression: block_template
|
||||||
"sigops": 0,
|
"sigops": 0,
|
||||||
"required": true
|
"required": true
|
||||||
},
|
},
|
||||||
|
"longpollid": "00016871043eab7f731654008728000000000000000000",
|
||||||
"target": "0000000000000000000000000000000000000000000000000000000000000001",
|
"target": "0000000000000000000000000000000000000000000000000000000000000001",
|
||||||
"mintime": 1654008606,
|
"mintime": 1654008606,
|
||||||
"mutable": [
|
"mutable": [
|
||||||
|
|
|
@ -25,6 +25,7 @@ expression: block_template
|
||||||
"sigops": 0,
|
"sigops": 0,
|
||||||
"required": true
|
"required": true
|
||||||
},
|
},
|
||||||
|
"longpollid": "00018424203eab7f731654008728000000000000000000",
|
||||||
"target": "0000000000000000000000000000000000000000000000000000000000000001",
|
"target": "0000000000000000000000000000000000000000000000000000000000000001",
|
||||||
"mintime": 1654008606,
|
"mintime": 1654008606,
|
||||||
"mutable": [
|
"mutable": [
|
||||||
|
|
|
@ -881,12 +881,6 @@ async fn rpc_getblocktemplate() {
|
||||||
|
|
||||||
use chrono::{TimeZone, Utc};
|
use chrono::{TimeZone, Utc};
|
||||||
|
|
||||||
use crate::methods::{
|
|
||||||
get_block_template_rpcs::constants::{
|
|
||||||
GET_BLOCK_TEMPLATE_MUTABLE_FIELD, GET_BLOCK_TEMPLATE_NONCE_RANGE_FIELD,
|
|
||||||
},
|
|
||||||
tests::utils::fake_history_tree,
|
|
||||||
};
|
|
||||||
use zebra_chain::{
|
use zebra_chain::{
|
||||||
amount::{Amount, NonNegative},
|
amount::{Amount, NonNegative},
|
||||||
block::{Hash, MAX_BLOCK_BYTES, ZCASH_BLOCK_VERSION},
|
block::{Hash, MAX_BLOCK_BYTES, ZCASH_BLOCK_VERSION},
|
||||||
|
@ -894,9 +888,19 @@ async fn rpc_getblocktemplate() {
|
||||||
work::difficulty::{CompactDifficulty, ExpandedDifficulty, U256},
|
work::difficulty::{CompactDifficulty, ExpandedDifficulty, U256},
|
||||||
};
|
};
|
||||||
use zebra_consensus::MAX_BLOCK_SIGOPS;
|
use zebra_consensus::MAX_BLOCK_SIGOPS;
|
||||||
|
|
||||||
use zebra_state::{GetBlockTemplateChainInfo, ReadRequest, ReadResponse};
|
use zebra_state::{GetBlockTemplateChainInfo, ReadRequest, ReadResponse};
|
||||||
|
|
||||||
|
use crate::methods::{
|
||||||
|
get_block_template_rpcs::{
|
||||||
|
constants::{
|
||||||
|
GET_BLOCK_TEMPLATE_CAPABILITIES_FIELD, GET_BLOCK_TEMPLATE_MUTABLE_FIELD,
|
||||||
|
GET_BLOCK_TEMPLATE_NONCE_RANGE_FIELD,
|
||||||
|
},
|
||||||
|
types::long_poll::LONG_POLL_ID_LENGTH,
|
||||||
|
},
|
||||||
|
tests::utils::fake_history_tree,
|
||||||
|
};
|
||||||
|
|
||||||
let _init_guard = zebra_test::init();
|
let _init_guard = zebra_test::init();
|
||||||
|
|
||||||
let mut mempool: MockService<_, _, _, BoxError> = MockService::build().for_unit_tests();
|
let mut mempool: MockService<_, _, _, BoxError> = MockService::build().for_unit_tests();
|
||||||
|
@ -973,7 +977,10 @@ async fn rpc_getblocktemplate() {
|
||||||
})
|
})
|
||||||
.expect("unexpected error in getblocktemplate RPC call");
|
.expect("unexpected error in getblocktemplate RPC call");
|
||||||
|
|
||||||
assert_eq!(get_block_template.capabilities, Vec::<String>::new());
|
assert_eq!(
|
||||||
|
get_block_template.capabilities,
|
||||||
|
GET_BLOCK_TEMPLATE_CAPABILITIES_FIELD.to_vec()
|
||||||
|
);
|
||||||
assert_eq!(get_block_template.version, ZCASH_BLOCK_VERSION);
|
assert_eq!(get_block_template.version, ZCASH_BLOCK_VERSION);
|
||||||
assert!(get_block_template.transactions.is_empty());
|
assert!(get_block_template.transactions.is_empty());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -1044,6 +1051,7 @@ async fn rpc_getblocktemplate() {
|
||||||
get_block_template_sync_error.code,
|
get_block_template_sync_error.code,
|
||||||
ErrorCode::ServerError(-10)
|
ErrorCode::ServerError(-10)
|
||||||
);
|
);
|
||||||
|
|
||||||
let get_block_template_sync_error = get_block_template_rpc
|
let get_block_template_sync_error = get_block_template_rpc
|
||||||
.get_block_template(Some(get_block_template_rpcs::types::get_block_template_opts::JsonParameters {
|
.get_block_template(Some(get_block_template_rpcs::types::get_block_template_opts::JsonParameters {
|
||||||
mode: get_block_template_rpcs::types::get_block_template_opts::GetBlockTemplateRequestMode::Proposal,
|
mode: get_block_template_rpcs::types::get_block_template_opts::GetBlockTemplateRequestMode::Proposal,
|
||||||
|
@ -1066,17 +1074,27 @@ async fn rpc_getblocktemplate() {
|
||||||
|
|
||||||
assert_eq!(get_block_template_sync_error.code, ErrorCode::InvalidParams);
|
assert_eq!(get_block_template_sync_error.code, ErrorCode::InvalidParams);
|
||||||
|
|
||||||
|
// The long poll id is valid, so it returns a state error instead
|
||||||
let get_block_template_sync_error = get_block_template_rpc
|
let get_block_template_sync_error = get_block_template_rpc
|
||||||
.get_block_template(Some(
|
.get_block_template(Some(
|
||||||
get_block_template_rpcs::types::get_block_template_opts::JsonParameters {
|
get_block_template_rpcs::types::get_block_template_opts::JsonParameters {
|
||||||
longpollid: Some("".to_string()),
|
// This must parse as a LongPollId.
|
||||||
|
// It must be the correct length and have hex/decimal digits.
|
||||||
|
longpollid: Some(
|
||||||
|
"0".repeat(LONG_POLL_ID_LENGTH)
|
||||||
|
.parse()
|
||||||
|
.expect("unexpected invalid LongPollId"),
|
||||||
|
),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
.await
|
.await
|
||||||
.expect_err("needs an error when using unsupported option");
|
.expect_err("needs an error when the state is empty");
|
||||||
|
|
||||||
assert_eq!(get_block_template_sync_error.code, ErrorCode::InvalidParams);
|
assert_eq!(
|
||||||
|
get_block_template_sync_error.code,
|
||||||
|
ErrorCode::ServerError(-10)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "getblocktemplate-rpcs")]
|
#[cfg(feature = "getblocktemplate-rpcs")]
|
||||||
|
|
|
@ -575,6 +575,7 @@ fn config_tests() -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test that `zebrad` runs the start command with no args
|
/// Test that `zebrad` runs the start command with no args
|
||||||
|
#[tracing::instrument]
|
||||||
fn app_no_args() -> Result<()> {
|
fn app_no_args() -> Result<()> {
|
||||||
let _init_guard = zebra_test::init();
|
let _init_guard = zebra_test::init();
|
||||||
|
|
||||||
|
@ -654,6 +655,8 @@ fn valid_generated_config(command: &str, expect_stdout_line_contains: &str) -> R
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the config produced by current zebrad is stored.
|
/// Check if the config produced by current zebrad is stored.
|
||||||
|
#[tracing::instrument]
|
||||||
|
#[allow(clippy::print_stdout)]
|
||||||
fn last_config_is_stored() -> Result<()> {
|
fn last_config_is_stored() -> Result<()> {
|
||||||
let _init_guard = zebra_test::init();
|
let _init_guard = zebra_test::init();
|
||||||
|
|
||||||
|
@ -709,14 +712,30 @@ fn last_config_is_stored() -> Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"\n\
|
||||||
|
Here is the missing config file: \n\
|
||||||
|
\n\
|
||||||
|
{processed_generated_content}\n"
|
||||||
|
);
|
||||||
|
|
||||||
Err(eyre!(
|
Err(eyre!(
|
||||||
"latest zebrad config is not being tested for compatibility. \n\
|
"latest zebrad config is not being tested for compatibility. \n\
|
||||||
Run: \n\
|
\n\
|
||||||
|
Take the missing config file logged above, \n\
|
||||||
|
and commit it to Zebra's git repository as:\n\
|
||||||
|
zebrad/tests/common/configs/{}<next-release-tag>.toml \n\
|
||||||
|
\n\
|
||||||
|
Or run: \n\
|
||||||
cargo build {}--bin zebrad && \n\
|
cargo build {}--bin zebrad && \n\
|
||||||
zebrad generate | \n\
|
zebrad generate | \n\
|
||||||
sed \"s/cache_dir = '.*'/cache_dir = 'cache_dir'/\" > \n\
|
sed \"s/cache_dir = '.*'/cache_dir = 'cache_dir'/\" > \n\
|
||||||
zebrad/tests/common/configs/{}<next-release-tag>.toml \n\
|
zebrad/tests/common/configs/{}<next-release-tag>.toml",
|
||||||
and commit the latest config to Zebra's git repository",
|
if cfg!(feature = "getblocktemplate-rpcs") {
|
||||||
|
GET_BLOCK_TEMPLATE_CONFIG_PREFIX
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
},
|
||||||
if cfg!(feature = "getblocktemplate-rpcs") {
|
if cfg!(feature = "getblocktemplate-rpcs") {
|
||||||
"--features=getblocktemplate-rpcs "
|
"--features=getblocktemplate-rpcs "
|
||||||
} else {
|
} else {
|
||||||
|
@ -732,6 +751,7 @@ fn last_config_is_stored() -> Result<()> {
|
||||||
|
|
||||||
/// Checks that Zebra prints an informative message when it cannot parse the
|
/// Checks that Zebra prints an informative message when it cannot parse the
|
||||||
/// config file.
|
/// config file.
|
||||||
|
#[tracing::instrument]
|
||||||
fn invalid_generated_config() -> Result<()> {
|
fn invalid_generated_config() -> Result<()> {
|
||||||
let _init_guard = zebra_test::init();
|
let _init_guard = zebra_test::init();
|
||||||
|
|
||||||
|
@ -804,6 +824,7 @@ fn invalid_generated_config() -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test all versions of `zebrad.toml` we have stored can be parsed by the latest `zebrad`.
|
/// Test all versions of `zebrad.toml` we have stored can be parsed by the latest `zebrad`.
|
||||||
|
#[tracing::instrument]
|
||||||
fn stored_configs_works() -> Result<()> {
|
fn stored_configs_works() -> Result<()> {
|
||||||
let old_configs_dir = configs_dir();
|
let old_configs_dir = configs_dir();
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
# Default configuration for zebrad.
|
||||||
|
#
|
||||||
|
# This file can be used as a skeleton for custom configs.
|
||||||
|
#
|
||||||
|
# Unspecified fields use default values. Optional fields are Some(field) if the
|
||||||
|
# field is present and None if it is absent.
|
||||||
|
#
|
||||||
|
# This file is generated as an example using zebrad's current defaults.
|
||||||
|
# You should set only the config options you want to keep, and delete the rest.
|
||||||
|
# Only a subset of fields are present in the skeleton, since optional values
|
||||||
|
# whose default is None are omitted.
|
||||||
|
#
|
||||||
|
# The config format (including a complete list of sections and fields) is
|
||||||
|
# documented here:
|
||||||
|
# https://doc.zebra.zfnd.org/zebrad/config/struct.ZebradConfig.html
|
||||||
|
#
|
||||||
|
# zebrad attempts to load configs in the following order:
|
||||||
|
#
|
||||||
|
# 1. The -c flag on the command line, e.g., `zebrad -c myconfig.toml start`;
|
||||||
|
# 2. The file `zebrad.toml` in the users's preference directory (platform-dependent);
|
||||||
|
# 3. The default config.
|
||||||
|
|
||||||
|
[consensus]
|
||||||
|
checkpoint_sync = true
|
||||||
|
debug_skip_parameter_preload = false
|
||||||
|
|
||||||
|
[mempool]
|
||||||
|
eviction_memory_time = '1h'
|
||||||
|
tx_cost_limit = 80000000
|
||||||
|
|
||||||
|
[metrics]
|
||||||
|
|
||||||
|
[mining]
|
||||||
|
|
||||||
|
[network]
|
||||||
|
crawl_new_peer_interval = '1m 1s'
|
||||||
|
initial_mainnet_peers = [
|
||||||
|
'dnsseed.z.cash:8233',
|
||||||
|
'dnsseed.str4d.xyz:8233',
|
||||||
|
'mainnet.seeder.zfnd.org:8233',
|
||||||
|
'mainnet.is.yolo.money:8233',
|
||||||
|
]
|
||||||
|
initial_testnet_peers = [
|
||||||
|
'dnsseed.testnet.z.cash:18233',
|
||||||
|
'testnet.seeder.zfnd.org:18233',
|
||||||
|
'testnet.is.yolo.money:18233',
|
||||||
|
]
|
||||||
|
listen_addr = '0.0.0.0:8233'
|
||||||
|
network = 'Mainnet'
|
||||||
|
peerset_initial_target_size = 25
|
||||||
|
|
||||||
|
[rpc]
|
||||||
|
debug_force_finished_sync = false
|
||||||
|
parallel_cpu_threads = 0
|
||||||
|
|
||||||
|
[state]
|
||||||
|
cache_dir = 'cache_dir'
|
||||||
|
delete_old_database = true
|
||||||
|
ephemeral = false
|
||||||
|
|
||||||
|
[sync]
|
||||||
|
checkpoint_verify_concurrency_limit = 1000
|
||||||
|
download_concurrency_limit = 50
|
||||||
|
full_verify_concurrency_limit = 20
|
||||||
|
parallel_cpu_threads = 0
|
||||||
|
|
||||||
|
[tracing]
|
||||||
|
buffer_limit = 128000
|
||||||
|
force_use_color = false
|
||||||
|
use_color = true
|
||||||
|
use_journald = false
|
||||||
|
|
Loading…
Reference in New Issue