feat(rpc): populate some getblocktemplate RPC block header fields using the state best chain tip (#5659)

* populate block height

* populate cur_time

* populate min_time

* populate capabilities

* populate last_block_hash

* create read state request for getblocktemplate

* refactor to get difficulty fields more properly

* populate bits and target fields

* fix tests

* add target and bits documentation

* docs

* fix docs

* docs

* remove metrixs counter calls

* apply some suggestions from code review

* hide some code behind feature

* simplify the service

* fix error handling

* remove comment

* fox doc

* panic if we dont have enough state

* bring tip data from the state

* make proposal empty

* fix time

* fix docs, consensus rules

* remove non used anymore fn

* remove another non used fn

* remove no needed change

* remove more unused changes

* remove unused anymore change

* apply suggestions from code review

Co-authored-by: teor <teor@riseup.net>

* fix build and snapshots

* apply testnet consensus rule

* fix test

* rustfmt

* remove time as allowed field to be modified by the miner if mining minimum difficulty block

* move all times to before calculating difficulty

* do some cleanup

* Adjust times so the whole time range is a testnet minimum difficulty block

* Return a GetBlockTemplateChainInfo struct from the difficulty calculation

* Add a Zebra-only max_time field to the getblocktemplate RPC

Co-authored-by: teor <teor@riseup.net>
This commit is contained in:
Alfredo Garcia 2022-11-28 06:06:32 -03:00 committed by GitHub
parent ea21e642dc
commit eb66f4b1a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 499 additions and 81 deletions

View File

@ -112,6 +112,17 @@ impl fmt::Debug for ExpandedDifficulty {
}
}
#[cfg(feature = "getblocktemplate-rpcs")]
impl fmt::Display for ExpandedDifficulty {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut buf = [0; 32];
// Use the same byte order as block::Hash
self.0.to_big_endian(&mut buf);
f.write_str(&hex::encode(buf))
}
}
/// A 128-bit unsigned "Work" value.
///
/// Used to calculate the total work for each chain of blocks.
@ -257,6 +268,12 @@ impl CompactDifficulty {
let expanded = self.to_expanded()?;
Work::try_from(expanded).ok()
}
#[cfg(feature = "getblocktemplate-rpcs")]
/// Returns the raw inner value.
pub fn to_value(&self) -> u32 {
self.0
}
}
impl TryFrom<ExpandedDifficulty> for Work {

View File

@ -27,6 +27,8 @@ use zebra_consensus::{
};
use zebra_node_services::mempool;
use zebra_state::{ReadRequest, ReadResponse};
use crate::methods::{
best_chain_tip_height,
get_block_template_rpcs::types::{
@ -275,6 +277,7 @@ where
let mempool = self.mempool.clone();
let latest_chain_tip = self.latest_chain_tip.clone();
let mut state = self.state.clone();
// Since this is a very large RPC, we use separate functions for each group of fields.
async move {
@ -286,6 +289,8 @@ where
data: None,
})?;
// The tip estimate may not be the same as the one coming from the state
// but this is ok for an estimate
let (estimated_distance_to_chain_tip, tip_height) = latest_chain_tip
.estimate_distance_to_network_chain_tip(network)
.ok_or_else(|| Error {
@ -314,7 +319,29 @@ where
let miner_fee = miner_fee(&mempool_txs);
// Calling state with `ChainInfo` request for relevant chain data
let request = ReadRequest::ChainInfo;
let response = state
.ready()
.and_then(|service| service.call(request))
.await
.map_err(|error| Error {
code: ErrorCode::ServerError(0),
message: error.to_string(),
data: None,
})?;
let chain_info = match response {
ReadResponse::ChainInfo(Some(chain_info)) => chain_info,
_ => unreachable!("we should always have enough state data here to get a `GetBlockTemplateChainInfo`"),
};
// Get the tip data from the state call
let tip_height = chain_info.tip.0;
let tip_hash = chain_info.tip.1;
let block_height = (tip_height + 1).expect("tip is far below Height::MAX");
let outputs =
standard_coinbase_outputs(network, block_height, miner_address, miner_fee);
let coinbase_tx = Transaction::new_v5_coinbase(network, block_height, outputs).into();
@ -325,13 +352,14 @@ where
// Convert into TransactionTemplates
let mempool_txs = mempool_txs.iter().map(Into::into).collect();
let empty_string = String::from("");
let mutable: Vec<String> = constants::GET_BLOCK_TEMPLATE_MUTABLE_FIELD.iter().map(ToString::to_string).collect();
Ok(GetBlockTemplate {
capabilities: vec![],
capabilities: Vec::new(),
version: ZCASH_BLOCK_VERSION,
previous_block_hash: GetBlockHash([0; 32].into()),
previous_block_hash: GetBlockHash(tip_hash),
block_commitments_hash: [0; 32].into(),
light_client_root_hash: [0; 32].into(),
final_sapling_root_hash: [0; 32].into(),
@ -346,14 +374,16 @@ where
coinbase_txn: TransactionTemplate::from_coinbase(&coinbase_tx, miner_fee),
target: empty_string.clone(),
target: format!(
"{}",
chain_info.expected_difficulty
.to_expanded()
.expect("state always returns a valid difficulty value")
),
min_time: 0,
min_time: chain_info.min_time.timestamp(),
mutable: constants::GET_BLOCK_TEMPLATE_MUTABLE_FIELD
.iter()
.map(ToString::to_string)
.collect(),
mutable,
nonce_range: constants::GET_BLOCK_TEMPLATE_NONCE_RANGE_FIELD.to_string(),
@ -361,11 +391,15 @@ where
size_limit: MAX_BLOCK_BYTES,
cur_time: 0,
cur_time: chain_info.current_system_time.timestamp(),
bits: empty_string,
bits: format!("{:#010x}", chain_info.expected_difficulty.to_value())
.drain(2..)
.collect(),
height: 0,
height: block_height.0,
max_time: chain_info.max_time.timestamp(),
})
}
.boxed()

View File

@ -18,6 +18,8 @@ pub struct GetBlockTemplate {
/// - `proposal`: <https://en.bitcoin.it/wiki/BIP_0023#Block_Proposal>
/// - `longpoll`: <https://en.bitcoin.it/wiki/BIP_0022#Optional:_Long_Polling>
/// - `serverlist`: <https://en.bitcoin.it/wiki/BIP_0023#Logical_Services>
///
/// By the above, Zebra will always return an empty vector here.
pub capabilities: Vec<String>,
/// The version of the block format.
@ -65,14 +67,17 @@ pub struct GetBlockTemplate {
#[serde(rename = "coinbasetxn")]
pub coinbase_txn: TransactionTemplate<amount::NegativeOrZero>,
/// Add documentation.
/// The expected difficulty for the new block displayed in expanded form.
// TODO: use ExpandedDifficulty type.
pub target: String,
/// Add documentation.
/// > For each block other than the genesis block, nTime MUST be strictly greater than
/// > the median-time-past of that block.
///
/// <https://zips.z.cash/protocol/protocol.pdf#blockheader>
#[serde(rename = "mintime")]
// TODO: use DateTime32 type?
pub min_time: u32,
pub min_time: i64,
/// Hardcoded list of block fields the miner is allowed to change.
pub mutable: Vec<String>,
@ -89,16 +94,29 @@ pub struct GetBlockTemplate {
#[serde(rename = "sizelimit")]
pub size_limit: u64,
/// Add documentation.
/// > the current time as seen by the server (recommended for block time).
/// > note this is not necessarily the system clock, and must fall within the mintime/maxtime rules
///
/// <https://en.bitcoin.it/wiki/BIP_0022#Block_Template_Request>
// TODO: use DateTime32 type?
#[serde(rename = "curtime")]
pub cur_time: u32,
pub cur_time: i64,
/// Add documentation.
/// The expected difficulty for the new block displayed in compact form.
// TODO: use CompactDifficulty type.
pub bits: String,
/// Add documentation.
/// The height of the next block in the best chain.
// TODO: use Height type?
pub height: u32,
/// Zebra adjusts the minimum and current times for testnet minimum difficulty blocks,
/// so we need to tell miners what the maximum valid time is.
///
/// This field is not in the Zcash RPC reference yet.
/// Currently, miners use `min_time` or `cur_time`, or calculate `max_time` from the
/// fixed 90 minute consensus rule. (Or they just don't check!)
#[serde(rename = "maxtime")]
// TODO: use DateTime32 type?
pub max_time: i64,
}

View File

@ -5,18 +5,24 @@
//! cargo insta test --review --features getblocktemplate-rpcs --delete-unreferenced-snapshots
//! ```
use chrono::{TimeZone, Utc};
use hex::FromHex;
use insta::Settings;
use tower::{buffer::Buffer, Service};
use zebra_chain::{
block::Hash,
chain_tip::mock::MockChainTip,
parameters::{Network, NetworkUpgrade},
serialization::ZcashDeserializeInto,
transaction::Transaction,
transparent,
work::difficulty::{CompactDifficulty, ExpandedDifficulty, U256},
};
use zebra_node_services::mempool;
use zebra_state::{GetBlockTemplateChainInfo, ReadRequest, ReadResponse};
use zebra_test::mock_service::{MockService, PanicAssertion};
use crate::methods::{
@ -75,17 +81,32 @@ pub async fn test_responses<State, ReadState>(
miner_address: Some(transparent::Address::from_script_hash(network, [0xad; 20])),
};
// nu5 block height
let fake_tip_height = NetworkUpgrade::Nu5.activation_height(network).unwrap();
// nu5 block hash
let fake_tip_hash =
Hash::from_hex("0000000000d723156d9b65ffcf4984da7a19675ed7e2f06d9e5d5188af087bf8").unwrap();
// nu5 block time + 1
let fake_min_time = Utc.timestamp_opt(1654008606, 0).unwrap();
// nu5 block time + 12
let fake_cur_time = Utc.timestamp_opt(1654008617, 0).unwrap();
// nu5 block time + 123
let fake_max_time = Utc.timestamp_opt(1654008728, 0).unwrap();
let (mock_chain_tip, mock_chain_tip_sender) = MockChainTip::new();
mock_chain_tip_sender.send_best_tip_height(NetworkUpgrade::Nu5.activation_height(network));
mock_chain_tip_sender.send_best_tip_height(fake_tip_height);
mock_chain_tip_sender.send_best_tip_hash(fake_tip_hash);
mock_chain_tip_sender.send_estimated_distance_to_network_chain_tip(Some(0));
// get an rpc instance with continuous blockchain state
let get_block_template_rpc = GetBlockTemplateRpcImpl::new(
network,
mining_config,
mining_config.clone(),
Buffer::new(mempool.clone(), 1),
read_state,
mock_chain_tip,
chain_verifier,
mock_chain_tip.clone(),
chain_verifier.clone(),
);
// `getblockcount`
@ -103,7 +124,39 @@ pub async fn test_responses<State, ReadState>(
snapshot_rpc_getblockhash(get_block_hash, &settings);
// get a new empty state
let new_read_state = MockService::build().for_unit_tests();
// send tip hash and time needed for getblocktemplate rpc
mock_chain_tip_sender.send_best_tip_hash(fake_tip_hash);
// create a new rpc instance with new state and mock
let get_block_template_rpc = GetBlockTemplateRpcImpl::new(
network,
mining_config,
Buffer::new(mempool.clone(), 1),
new_read_state.clone(),
mock_chain_tip,
chain_verifier,
);
// `getblocktemplate`
// Fake the ChainInfo response
tokio::spawn(async move {
new_read_state
.clone()
.expect_request_that(|req| matches!(req, ReadRequest::ChainInfo))
.await
.respond(ReadResponse::ChainInfo(Some(GetBlockTemplateChainInfo {
expected_difficulty: CompactDifficulty::from(ExpandedDifficulty::from(U256::one())),
tip: (fake_tip_height, fake_tip_hash),
current_system_time: fake_cur_time,
min_time: fake_min_time,
max_time: fake_max_time,
})));
});
let get_block_template = tokio::spawn(get_block_template_rpc.get_block_template());
mempool

View File

@ -5,7 +5,7 @@ expression: block_template
{
"capabilities": [],
"version": 4,
"previousblockhash": "0000000000000000000000000000000000000000000000000000000000000000",
"previousblockhash": "0000000000d723156d9b65ffcf4984da7a19675ed7e2f06d9e5d5188af087bf8",
"blockcommitmentshash": "0000000000000000000000000000000000000000000000000000000000000000",
"lightclientroothash": "0000000000000000000000000000000000000000000000000000000000000000",
"finalsaplingroothash": "0000000000000000000000000000000000000000000000000000000000000000",
@ -25,8 +25,8 @@ expression: block_template
"sigops": 0,
"required": true
},
"target": "",
"mintime": 0,
"target": "0000000000000000000000000000000000000000000000000000000000000001",
"mintime": 1654008606,
"mutable": [
"time",
"transactions",
@ -35,7 +35,8 @@ expression: block_template
"noncerange": "00000000ffffffff",
"sigoplimit": 20000,
"sizelimit": 2000000,
"curtime": 0,
"bits": "",
"height": 0
"curtime": 1654008617,
"bits": "01010000",
"height": 1687105,
"maxtime": 1654008728
}

View File

@ -5,7 +5,7 @@ expression: block_template
{
"capabilities": [],
"version": 4,
"previousblockhash": "0000000000000000000000000000000000000000000000000000000000000000",
"previousblockhash": "0000000000d723156d9b65ffcf4984da7a19675ed7e2f06d9e5d5188af087bf8",
"blockcommitmentshash": "0000000000000000000000000000000000000000000000000000000000000000",
"lightclientroothash": "0000000000000000000000000000000000000000000000000000000000000000",
"finalsaplingroothash": "0000000000000000000000000000000000000000000000000000000000000000",
@ -25,8 +25,8 @@ expression: block_template
"sigops": 0,
"required": true
},
"target": "",
"mintime": 0,
"target": "0000000000000000000000000000000000000000000000000000000000000001",
"mintime": 1654008606,
"mutable": [
"time",
"transactions",
@ -35,7 +35,8 @@ expression: block_template
"noncerange": "00000000ffffffff",
"sigoplimit": 20000,
"sizelimit": 2000000,
"curtime": 0,
"bits": "",
"height": 0
"curtime": 1654008617,
"bits": "01010000",
"height": 1842421,
"maxtime": 1654008728
}

View File

@ -787,48 +787,47 @@ async fn rpc_getblockhash() {
async fn rpc_getblocktemplate() {
use std::panic;
use chrono::{TimeZone, Utc};
use crate::methods::get_block_template_rpcs::constants::{
GET_BLOCK_TEMPLATE_MUTABLE_FIELD, GET_BLOCK_TEMPLATE_NONCE_RANGE_FIELD,
};
use zebra_chain::{
amount::{Amount, NonNegative},
block::{MAX_BLOCK_BYTES, ZCASH_BLOCK_VERSION},
block::{Hash, MAX_BLOCK_BYTES, ZCASH_BLOCK_VERSION},
chain_tip::mock::MockChainTip,
work::difficulty::{CompactDifficulty, ExpandedDifficulty, U256},
};
use zebra_consensus::MAX_BLOCK_SIGOPS;
use zebra_state::{GetBlockTemplateChainInfo, ReadRequest, ReadResponse};
let _init_guard = zebra_test::init();
// Create a continuous chain of mainnet blocks from genesis
let blocks: Vec<Arc<Block>> = zebra_test::vectors::CONTINUOUS_MAINNET_BLOCKS
.iter()
.map(|(_height, block_bytes)| block_bytes.zcash_deserialize_into().unwrap())
.collect();
let mut mempool: MockService<_, _, _, BoxError> = MockService::build().for_unit_tests();
// Create a populated state service
let (state, read_state, _latest_chain_tip, _chain_tip_change) =
zebra_state::populated_state(blocks.clone(), Mainnet).await;
let (
chain_verifier,
_transaction_verifier,
_parameter_download_task_handle,
_max_checkpoint_height,
) = zebra_consensus::chain::init(
zebra_consensus::Config::default(),
Mainnet,
state.clone(),
true,
)
.await;
let read_state = MockService::build().for_unit_tests();
let chain_verifier = MockService::build().for_unit_tests();
let mining_config = get_block_template_rpcs::config::Config {
miner_address: Some(transparent::Address::from_script_hash(Mainnet, [0x7e; 20])),
};
// nu5 block height
let fake_tip_height = NetworkUpgrade::Nu5.activation_height(Mainnet).unwrap();
// nu5 block hash
let fake_tip_hash =
Hash::from_hex("0000000000d723156d9b65ffcf4984da7a19675ed7e2f06d9e5d5188af087bf8").unwrap();
// nu5 block time + 1
let fake_min_time = Utc.timestamp_opt(1654008606, 0).unwrap();
// nu5 block time + 12
let fake_cur_time = Utc.timestamp_opt(1654008617, 0).unwrap();
// nu5 block time + 123
let fake_max_time = Utc.timestamp_opt(1654008728, 0).unwrap();
let (mock_chain_tip, mock_chain_tip_sender) = MockChainTip::new();
mock_chain_tip_sender.send_best_tip_height(NetworkUpgrade::Nu5.activation_height(Mainnet));
mock_chain_tip_sender.send_best_tip_height(fake_tip_height);
mock_chain_tip_sender.send_best_tip_hash(fake_tip_hash);
mock_chain_tip_sender.send_estimated_distance_to_network_chain_tip(Some(0));
// Init RPC
@ -836,11 +835,26 @@ async fn rpc_getblocktemplate() {
Mainnet,
mining_config,
Buffer::new(mempool.clone(), 1),
read_state,
read_state.clone(),
mock_chain_tip,
tower::ServiceBuilder::new().service(chain_verifier),
);
// Fake the ChainInfo response
tokio::spawn(async move {
read_state
.clone()
.expect_request_that(|req| matches!(req, ReadRequest::ChainInfo))
.await
.respond(ReadResponse::ChainInfo(Some(GetBlockTemplateChainInfo {
expected_difficulty: CompactDifficulty::from(ExpandedDifficulty::from(U256::one())),
tip: (fake_tip_height, fake_tip_hash),
current_system_time: fake_cur_time,
min_time: fake_min_time,
max_time: fake_max_time,
})));
});
let get_block_template = tokio::spawn(get_block_template_rpc.get_block_template());
mempool
@ -858,11 +872,14 @@ async fn rpc_getblocktemplate() {
})
.expect("unexpected error in getblocktemplate RPC call");
assert!(get_block_template.capabilities.is_empty());
assert_eq!(get_block_template.capabilities, Vec::<String>::new());
assert_eq!(get_block_template.version, ZCASH_BLOCK_VERSION);
assert!(get_block_template.transactions.is_empty());
assert!(get_block_template.target.is_empty());
assert_eq!(get_block_template.min_time, 0);
assert_eq!(
get_block_template.target,
"0000000000000000000000000000000000000000000000000000000000000001"
);
assert_eq!(get_block_template.min_time, fake_min_time.timestamp());
assert_eq!(
get_block_template.mutable,
GET_BLOCK_TEMPLATE_MUTABLE_FIELD.to_vec()
@ -873,9 +890,10 @@ async fn rpc_getblocktemplate() {
);
assert_eq!(get_block_template.sigop_limit, MAX_BLOCK_SIGOPS);
assert_eq!(get_block_template.size_limit, MAX_BLOCK_BYTES);
assert_eq!(get_block_template.cur_time, 0);
assert!(get_block_template.bits.is_empty());
assert_eq!(get_block_template.height, 0);
assert_eq!(get_block_template.cur_time, fake_cur_time.timestamp());
assert_eq!(get_block_template.bits, "01010000");
assert_eq!(get_block_template.height, 1687105); // nu5 height
assert_eq!(get_block_template.max_time, fake_max_time.timestamp());
// Coinbase transaction checks.
assert!(get_block_template.coinbase_txn.required);

View File

@ -32,6 +32,8 @@ pub use config::{check_and_delete_old_databases, Config};
pub use constants::MAX_BLOCK_REORG_HEIGHT;
pub use error::{BoxError, CloneError, CommitBlockError, ValidateContextError};
pub use request::{FinalizedBlock, HashOrHeight, PreparedBlock, ReadRequest, Request};
#[cfg(feature = "getblocktemplate-rpcs")]
pub use response::GetBlockTemplateChainInfo;
pub use response::{ReadResponse, Response};
pub use service::{
chain_tip::{ChainTipChange, LatestChainTip, TipAction},

View File

@ -744,6 +744,14 @@ pub enum ReadRequest {
/// * [`ReadResponse::BlockHash(Some(hash))`](ReadResponse::BlockHash) if the block is in the best chain;
/// * [`ReadResponse::BlockHash(None)`](ReadResponse::BlockHash) otherwise.
BestChainBlockHash(block::Height),
#[cfg(feature = "getblocktemplate-rpcs")]
/// Get state information from the best block chain.
///
/// Returns [`ReadResponse::ChainInfo(info)`](ReadResponse::ChainInfo) where `info` is a
/// [`zebra-state::GetBlockTemplateChainInfo`](zebra-state::GetBlockTemplateChainInfo)` structure containing
/// best chain state information.
ChainInfo,
}
impl ReadRequest {
@ -766,6 +774,8 @@ impl ReadRequest {
ReadRequest::UtxosByAddresses(_) => "utxos_by_addesses",
#[cfg(feature = "getblocktemplate-rpcs")]
ReadRequest::BestChainBlockHash(_) => "best_chain_block_hash",
#[cfg(feature = "getblocktemplate-rpcs")]
ReadRequest::ChainInfo => "chain_info",
}
}

View File

@ -10,6 +10,9 @@ use zebra_chain::{
transparent,
};
#[cfg(feature = "getblocktemplate-rpcs")]
use zebra_chain::work::difficulty::CompactDifficulty;
// Allow *only* these unused imports, so that rustdoc link resolution
// will work with inline links.
#[allow(unused_imports)]
@ -115,6 +118,32 @@ pub enum ReadResponse {
/// Response to [`ReadRequest::BestChainBlockHash`](crate::ReadRequest::BestChainBlockHash) with the
/// specified block hash.
BlockHash(Option<block::Hash>),
#[cfg(feature = "getblocktemplate-rpcs")]
/// Response to [`ReadRequest::ChainInfo`](crate::ReadRequest::ChainInfo) with the state
/// information needed by the `getblocktemplate` RPC method.
ChainInfo(Option<GetBlockTemplateChainInfo>),
}
#[cfg(feature = "getblocktemplate-rpcs")]
/// A structure with the information needed from the state to build a `getblocktemplate` RPC response.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GetBlockTemplateChainInfo {
/// The current state tip height and hash.
/// The block template for the candidate block is the next block after this block.
pub tip: (block::Height, block::Hash),
/// The expected difficulty of the candidate block.
pub expected_difficulty: CompactDifficulty,
/// The current system time, adjusted to fit within `min_time` and `max_time`.
pub current_system_time: chrono::DateTime<chrono::Utc>,
/// The mininimum time the miner can use in this block.
pub min_time: chrono::DateTime<chrono::Utc>,
/// The maximum time the miner can use in this block.
pub max_time: chrono::DateTime<chrono::Utc>,
}
/// Conversion from read-only [`ReadResponse`]s to read-write [`Response`]s.
@ -155,6 +184,10 @@ impl TryFrom<ReadResponse> for Response {
ReadResponse::BlockHash(_) => {
Err("there is no corresponding Response for this ReadResponse")
}
#[cfg(feature = "getblocktemplate-rpcs")]
ReadResponse::ChainInfo(_) => {
Err("there is no corresponding Response for this ReadResponse")
}
}
}
}

View File

@ -1522,13 +1522,6 @@ impl Service<ReadRequest> for ReadStateService {
// Used by get_block_hash RPC.
#[cfg(feature = "getblocktemplate-rpcs")]
ReadRequest::BestChainBlockHash(height) => {
metrics::counter!(
"state.requests",
1,
"service" => "read_state",
"type" => "best_chain_block_hash",
);
let timer = CodeTimer::start();
let state = self.clone();
@ -1554,6 +1547,53 @@ impl Service<ReadRequest> for ReadStateService {
.map(|join_result| join_result.expect("panic in ReadRequest::BestChainBlockHash"))
.boxed()
}
// Used by get_block_template RPC.
#[cfg(feature = "getblocktemplate-rpcs")]
ReadRequest::ChainInfo => {
let timer = CodeTimer::start();
let state = self.clone();
let latest_non_finalized_state = self.latest_non_finalized_state();
// # Performance
//
// Allow other async tasks to make progress while concurrently reading blocks from disk.
let span = Span::current();
tokio::task::spawn_blocking(move || {
span.in_scope(move || {
// # Correctness
//
// It is ok to do these lookups using multiple database calls. Finalized state updates
// can only add overlapping blocks, and block hashes are unique across all chain forks.
//
// If there is a large overlap between the non-finalized and finalized states,
// where the finalized tip is above the non-finalized tip,
// Zebra is receiving a lot of blocks, or this request has been delayed for a long time.
//
// In that case, the `getblocktemplate` RPC will return an error because Zebra
// is not synced to the tip. That check happens before the RPC makes this request.
let get_block_template_info =
read::tip(latest_non_finalized_state.best_chain(), &state.db).map(
|tip| {
read::difficulty::difficulty_and_time_info(
&latest_non_finalized_state,
&state.db,
tip,
state.network,
)
},
);
// The work is done in the future.
timer.finish(module_path!(), line!(), "ReadRequest::ChainInfo");
Ok(ReadResponse::ChainInfo(get_block_template_info))
})
})
.map(|join_result| join_result.expect("panic in ReadRequest::ChainInfo"))
.boxed()
}
}
}
}

View File

@ -35,7 +35,7 @@ pub(crate) mod utxo;
#[cfg(test)]
mod tests;
use difficulty::{AdjustedDifficulty, POW_MEDIAN_BLOCK_SPAN};
pub(crate) use difficulty::{AdjustedDifficulty, POW_MEDIAN_BLOCK_SPAN};
/// Check that the `prepared` block is contextually valid for `network`, based
/// on the `finalized_tip_height` and `relevant_chain`.

View File

@ -46,7 +46,7 @@ pub const POW_MAX_ADJUST_DOWN_PERCENT: i32 = 32;
pub const BLOCK_MAX_TIME_SINCE_MEDIAN: i64 = 90 * 60;
/// Contains the context needed to calculate the adjusted difficulty for a block.
pub(super) struct AdjustedDifficulty {
pub(crate) struct AdjustedDifficulty {
/// The `header.time` field from the candidate block
candidate_time: DateTime<Utc>,
/// The coinbase height from the candidate block
@ -99,8 +99,8 @@ impl AdjustedDifficulty {
let previous_block_height = (candidate_block_height - 1)
.expect("contextual validation is never run on the genesis block");
AdjustedDifficulty::new_from_header(
&candidate_block.header,
AdjustedDifficulty::new_from_header_time(
candidate_block.header.time,
previous_block_height,
network,
context,
@ -108,7 +108,7 @@ impl AdjustedDifficulty {
}
/// Initialise and return a new [`AdjustedDifficulty`] using a
/// `candidate_header`, `previous_block_height`, `network`, and a `context`.
/// `candidate_header_time`, `previous_block_height`, `network`, and a `context`.
///
/// Designed for use when validating block headers, where the full block has not
/// been downloaded yet.
@ -118,8 +118,8 @@ impl AdjustedDifficulty {
/// # Panics
///
/// If the context contains fewer than 28 items.
pub fn new_from_header<C>(
candidate_header: &block::Header,
pub fn new_from_header_time<C>(
candidate_header_time: DateTime<Utc>,
previous_block_height: block::Height,
network: Network,
context: C,
@ -142,7 +142,7 @@ impl AdjustedDifficulty {
.expect("not enough context: difficulty adjustment needs at least 28 (PoWAveragingWindow + PoWMedianBlockSpan) headers");
AdjustedDifficulty {
candidate_time: candidate_header.time,
candidate_time: candidate_header_time,
candidate_height,
network,
relevant_difficulty_thresholds,

View File

@ -16,6 +16,8 @@ use crate::service;
pub mod address;
pub mod block;
#[cfg(feature = "getblocktemplate-rpcs")]
pub mod difficulty;
pub mod find;
pub mod tree;
@ -33,7 +35,7 @@ pub use block::{
};
#[cfg(feature = "getblocktemplate-rpcs")]
pub use block::hash;
pub use {block::hash, difficulty::difficulty_and_time_info};
pub use find::{
best_tip, block_locator, chain_contains_hash, depth, find_chain_hashes, find_chain_headers,

View File

@ -0,0 +1,189 @@
//! Get context and calculate difficulty for the next block.
use std::borrow::Borrow;
use chrono::{DateTime, Duration, TimeZone, Utc};
use zebra_chain::{
block::{Block, Hash, Height},
parameters::{Network, NetworkUpgrade, POW_AVERAGING_WINDOW},
work::difficulty::{CompactDifficulty, ExpandedDifficulty},
};
use crate::{
service::{
any_ancestor_blocks,
check::{
difficulty::{BLOCK_MAX_TIME_SINCE_MEDIAN, POW_MEDIAN_BLOCK_SPAN},
AdjustedDifficulty,
},
finalized_state::ZebraDb,
NonFinalizedState,
},
GetBlockTemplateChainInfo,
};
/// Returns :
/// - The `CompactDifficulty`, for the current best chain.
/// - The current system time.
/// - The minimum time for a next block.
///
/// Panic if we don't have enough blocks in the state.
pub fn difficulty_and_time_info(
non_finalized_state: &NonFinalizedState,
db: &ZebraDb,
tip: (Height, Hash),
network: Network,
) -> GetBlockTemplateChainInfo {
let relevant_chain = any_ancestor_blocks(non_finalized_state, db, tip.1);
difficulty_and_time(relevant_chain, tip, network)
}
fn difficulty_and_time<C>(
relevant_chain: C,
tip: (Height, Hash),
network: Network,
) -> GetBlockTemplateChainInfo
where
C: IntoIterator,
C::Item: Borrow<Block>,
C::IntoIter: ExactSizeIterator,
{
const MAX_CONTEXT_BLOCKS: usize = POW_AVERAGING_WINDOW + POW_MEDIAN_BLOCK_SPAN;
let relevant_chain: Vec<_> = relevant_chain
.into_iter()
.take(MAX_CONTEXT_BLOCKS)
.collect();
let relevant_data: Vec<(CompactDifficulty, DateTime<Utc>)> = relevant_chain
.iter()
.map(|block| {
(
block.borrow().header.difficulty_threshold,
block.borrow().header.time,
)
})
.collect();
// The getblocktemplate RPC returns an error if Zebra is not synced to the tip.
// So this will never happen in production code.
assert!(relevant_data.len() < MAX_CONTEXT_BLOCKS);
let current_system_time = chrono::Utc::now();
// Get the median-time-past, which doesn't depend on the current system time.
//
// TODO: split out median-time-past into its own struct?
let median_time_past = AdjustedDifficulty::new_from_header_time(
current_system_time,
tip.0,
network,
relevant_data.clone(),
)
.median_time_past();
// > For each block other than the genesis block , nTime MUST be strictly greater than
// > the median-time-past of that block.
// https://zips.z.cash/protocol/protocol.pdf#blockheader
let mut min_time = median_time_past
.checked_add_signed(Duration::seconds(1))
.expect("median time plus a small constant is far below i64::MAX");
// > For each block at block height 2 or greater on Mainnet, or block height 653606 or greater on Testnet, nTime
// > MUST be less than or equal to the median-time-past of that block plus 90 * 60 seconds.
//
// We ignore the height as we are checkpointing on Canopy or higher in Mainnet and Testnet.
let max_time = median_time_past
.checked_add_signed(Duration::seconds(BLOCK_MAX_TIME_SINCE_MEDIAN))
.expect("median time plus a small constant is far below i64::MAX");
let current_system_time = current_system_time
.timestamp()
.clamp(min_time.timestamp(), max_time.timestamp());
let mut current_system_time = Utc.timestamp_opt(current_system_time, 0).single().expect(
"clamping a timestamp between two valid times can't make it invalid, and \
UTC never has ambiguous time zone conversions",
);
// Now that we have a valid time, get the difficulty for that time.
let mut difficulty_adjustment = AdjustedDifficulty::new_from_header_time(
current_system_time,
tip.0,
network,
relevant_data.iter().cloned(),
);
// On testnet, changing the block time can also change the difficulty,
// due to the minimum difficulty consensus rule:
// > if the block time of a block at height height ≥ 299188
// > is greater than 6 * PoWTargetSpacing(height) seconds after that of the preceding block,
// > then the block is a minimum-difficulty block.
//
// In this case, we adjust the min_time and cur_time to the first minimum difficulty time.
//
// In rare cases, this could make some testnet miners produce invalid blocks,
// if they use the full 90 minute time gap in the consensus rules.
// (The getblocktemplate RPC reference doesn't have a max_time field,
// so there is no standard way of telling miners that the max_time is smaller.)
//
// But that's better than obscure failures caused by changing the time a small amount,
// if that moves the block from standard to minimum difficulty.
if network == Network::Testnet {
let max_time_difficulty_adjustment = AdjustedDifficulty::new_from_header_time(
max_time,
tip.0,
network,
relevant_data.iter().cloned(),
);
// The max time is a minimum difficulty block,
// so the time range could have different difficulties.
if max_time_difficulty_adjustment.expected_difficulty_threshold()
== ExpandedDifficulty::target_difficulty_limit(Network::Testnet).to_compact()
{
let min_time_difficulty_adjustment = AdjustedDifficulty::new_from_header_time(
min_time,
tip.0,
network,
relevant_data.iter().cloned(),
);
// Part of the valid range has a different difficulty.
// So we need to find the minimum time that is also a minimum difficulty block.
// This is the valid range for miners.
if min_time_difficulty_adjustment.expected_difficulty_threshold()
!= max_time_difficulty_adjustment.expected_difficulty_threshold()
{
let preceding_block_time = relevant_data.last().expect("has at least one block").1;
let minimum_difficulty_spacing =
NetworkUpgrade::minimum_difficulty_spacing_for_height(network, tip.0)
.expect("just checked the minimum difficulty rule is active");
// The first minimum difficulty time is strictly greater than the spacing.
min_time = preceding_block_time + minimum_difficulty_spacing + Duration::seconds(1);
// Update the difficulty and times to match
if current_system_time < min_time {
current_system_time = min_time;
}
difficulty_adjustment = AdjustedDifficulty::new_from_header_time(
current_system_time,
tip.0,
network,
relevant_data,
);
}
}
}
GetBlockTemplateChainInfo {
tip,
expected_difficulty: difficulty_adjustment.expected_difficulty_threshold(),
min_time,
current_system_time,
max_time,
}
}