From eb66f4b1a386e8f6e5836bce29739b06c14ae9ca Mon Sep 17 00:00:00 2001 From: Alfredo Garcia Date: Mon, 28 Nov 2022 06:06:32 -0300 Subject: [PATCH] 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 * 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 --- zebra-chain/src/work/difficulty.rs | 17 ++ .../src/methods/get_block_template_rpcs.rs | 58 ++++-- .../types/get_block_template.rs | 32 ++- .../tests/snapshot/get_block_template_rpcs.rs | 61 +++++- .../get_block_template@mainnet_10.snap | 13 +- .../get_block_template@testnet_10.snap | 13 +- zebra-rpc/src/methods/tests/vectors.rs | 78 +++++--- zebra-state/src/lib.rs | 2 + zebra-state/src/request.rs | 10 + zebra-state/src/response.rs | 33 +++ zebra-state/src/service.rs | 54 ++++- zebra-state/src/service/check.rs | 2 +- zebra-state/src/service/check/difficulty.rs | 14 +- zebra-state/src/service/read.rs | 4 +- zebra-state/src/service/read/difficulty.rs | 189 ++++++++++++++++++ 15 files changed, 499 insertions(+), 81 deletions(-) create mode 100644 zebra-state/src/service/read/difficulty.rs diff --git a/zebra-chain/src/work/difficulty.rs b/zebra-chain/src/work/difficulty.rs index 00478e38..37902076 100644 --- a/zebra-chain/src/work/difficulty.rs +++ b/zebra-chain/src/work/difficulty.rs @@ -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 for Work { diff --git a/zebra-rpc/src/methods/get_block_template_rpcs.rs b/zebra-rpc/src/methods/get_block_template_rpcs.rs index 9b8a1bbb..5b1d0a63 100644 --- a/zebra-rpc/src/methods/get_block_template_rpcs.rs +++ b/zebra-rpc/src/methods/get_block_template_rpcs.rs @@ -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 = 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() diff --git a/zebra-rpc/src/methods/get_block_template_rpcs/types/get_block_template.rs b/zebra-rpc/src/methods/get_block_template_rpcs/types/get_block_template.rs index 63840d15..7daad104 100644 --- a/zebra-rpc/src/methods/get_block_template_rpcs/types/get_block_template.rs +++ b/zebra-rpc/src/methods/get_block_template_rpcs/types/get_block_template.rs @@ -18,6 +18,8 @@ pub struct GetBlockTemplate { /// - `proposal`: /// - `longpoll`: /// - `serverlist`: + /// + /// By the above, Zebra will always return an empty vector here. pub capabilities: Vec, /// The version of the block format. @@ -65,14 +67,17 @@ pub struct GetBlockTemplate { #[serde(rename = "coinbasetxn")] pub coinbase_txn: TransactionTemplate, - /// 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. + /// + /// #[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, @@ -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 + /// + /// // 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, } diff --git a/zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs b/zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs index 1b54844c..8216c266 100644 --- a/zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs +++ b/zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs @@ -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( 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( 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 diff --git a/zebra-rpc/src/methods/tests/snapshot/snapshots/get_block_template@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshot/snapshots/get_block_template@mainnet_10.snap index 6a701693..b1ba9fa2 100644 --- a/zebra-rpc/src/methods/tests/snapshot/snapshots/get_block_template@mainnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshot/snapshots/get_block_template@mainnet_10.snap @@ -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 } diff --git a/zebra-rpc/src/methods/tests/snapshot/snapshots/get_block_template@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshot/snapshots/get_block_template@testnet_10.snap index 10ac2ec5..cdc0d129 100644 --- a/zebra-rpc/src/methods/tests/snapshot/snapshots/get_block_template@testnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshot/snapshots/get_block_template@testnet_10.snap @@ -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 } diff --git a/zebra-rpc/src/methods/tests/vectors.rs b/zebra-rpc/src/methods/tests/vectors.rs index 09262f1a..e0c8e75a 100644 --- a/zebra-rpc/src/methods/tests/vectors.rs +++ b/zebra-rpc/src/methods/tests/vectors.rs @@ -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> = 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::::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); diff --git a/zebra-state/src/lib.rs b/zebra-state/src/lib.rs index f5421217..5355b66e 100644 --- a/zebra-state/src/lib.rs +++ b/zebra-state/src/lib.rs @@ -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}, diff --git a/zebra-state/src/request.rs b/zebra-state/src/request.rs index 151bb769..cf298b50 100644 --- a/zebra-state/src/request.rs +++ b/zebra-state/src/request.rs @@ -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", } } diff --git a/zebra-state/src/response.rs b/zebra-state/src/response.rs index 71d9ff10..fe1d820a 100644 --- a/zebra-state/src/response.rs +++ b/zebra-state/src/response.rs @@ -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), + + #[cfg(feature = "getblocktemplate-rpcs")] + /// Response to [`ReadRequest::ChainInfo`](crate::ReadRequest::ChainInfo) with the state + /// information needed by the `getblocktemplate` RPC method. + ChainInfo(Option), +} + +#[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, + + /// The mininimum time the miner can use in this block. + pub min_time: chrono::DateTime, + + /// The maximum time the miner can use in this block. + pub max_time: chrono::DateTime, } /// Conversion from read-only [`ReadResponse`]s to read-write [`Response`]s. @@ -155,6 +184,10 @@ impl TryFrom 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") + } } } } diff --git a/zebra-state/src/service.rs b/zebra-state/src/service.rs index 0f45aa0f..a0ddcba9 100644 --- a/zebra-state/src/service.rs +++ b/zebra-state/src/service.rs @@ -1522,13 +1522,6 @@ impl Service 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 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() + } } } } diff --git a/zebra-state/src/service/check.rs b/zebra-state/src/service/check.rs index e540941f..2ced5deb 100644 --- a/zebra-state/src/service/check.rs +++ b/zebra-state/src/service/check.rs @@ -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`. diff --git a/zebra-state/src/service/check/difficulty.rs b/zebra-state/src/service/check/difficulty.rs index a7973b45..fb17a398 100644 --- a/zebra-state/src/service/check/difficulty.rs +++ b/zebra-state/src/service/check/difficulty.rs @@ -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, /// 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( - candidate_header: &block::Header, + pub fn new_from_header_time( + candidate_header_time: DateTime, 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, diff --git a/zebra-state/src/service/read.rs b/zebra-state/src/service/read.rs index b0579c83..2507daa7 100644 --- a/zebra-state/src/service/read.rs +++ b/zebra-state/src/service/read.rs @@ -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, diff --git a/zebra-state/src/service/read/difficulty.rs b/zebra-state/src/service/read/difficulty.rs new file mode 100644 index 00000000..fcebbc7a --- /dev/null +++ b/zebra-state/src/service/read/difficulty.rs @@ -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( + relevant_chain: C, + tip: (Height, Hash), + network: Network, +) -> GetBlockTemplateChainInfo +where + C: IntoIterator, + C::Item: Borrow, + 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)> = 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, + } +}