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:
parent
ea21e642dc
commit
eb66f4b1a3
|
@ -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.
|
/// A 128-bit unsigned "Work" value.
|
||||||
///
|
///
|
||||||
/// Used to calculate the total work for each chain of blocks.
|
/// Used to calculate the total work for each chain of blocks.
|
||||||
|
@ -257,6 +268,12 @@ impl CompactDifficulty {
|
||||||
let expanded = self.to_expanded()?;
|
let expanded = self.to_expanded()?;
|
||||||
Work::try_from(expanded).ok()
|
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 {
|
impl TryFrom<ExpandedDifficulty> for Work {
|
||||||
|
|
|
@ -27,6 +27,8 @@ use zebra_consensus::{
|
||||||
};
|
};
|
||||||
use zebra_node_services::mempool;
|
use zebra_node_services::mempool;
|
||||||
|
|
||||||
|
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::types::{
|
||||||
|
@ -275,6 +277,7 @@ where
|
||||||
|
|
||||||
let mempool = self.mempool.clone();
|
let mempool = self.mempool.clone();
|
||||||
let latest_chain_tip = self.latest_chain_tip.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.
|
// Since this is a very large RPC, we use separate functions for each group of fields.
|
||||||
async move {
|
async move {
|
||||||
|
@ -286,6 +289,8 @@ where
|
||||||
data: None,
|
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
|
let (estimated_distance_to_chain_tip, 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 {
|
||||||
|
@ -314,7 +319,29 @@ where
|
||||||
|
|
||||||
let miner_fee = miner_fee(&mempool_txs);
|
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 block_height = (tip_height + 1).expect("tip is far below Height::MAX");
|
||||||
|
|
||||||
let outputs =
|
let outputs =
|
||||||
standard_coinbase_outputs(network, block_height, miner_address, miner_fee);
|
standard_coinbase_outputs(network, block_height, miner_address, miner_fee);
|
||||||
let coinbase_tx = Transaction::new_v5_coinbase(network, block_height, outputs).into();
|
let coinbase_tx = Transaction::new_v5_coinbase(network, block_height, outputs).into();
|
||||||
|
@ -325,13 +352,14 @@ where
|
||||||
// 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 empty_string = String::from("");
|
let mutable: Vec<String> = constants::GET_BLOCK_TEMPLATE_MUTABLE_FIELD.iter().map(ToString::to_string).collect();
|
||||||
|
|
||||||
Ok(GetBlockTemplate {
|
Ok(GetBlockTemplate {
|
||||||
capabilities: vec![],
|
capabilities: Vec::new(),
|
||||||
|
|
||||||
version: ZCASH_BLOCK_VERSION,
|
version: ZCASH_BLOCK_VERSION,
|
||||||
|
|
||||||
previous_block_hash: GetBlockHash([0; 32].into()),
|
previous_block_hash: GetBlockHash(tip_hash),
|
||||||
block_commitments_hash: [0; 32].into(),
|
block_commitments_hash: [0; 32].into(),
|
||||||
light_client_root_hash: [0; 32].into(),
|
light_client_root_hash: [0; 32].into(),
|
||||||
final_sapling_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),
|
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
|
mutable,
|
||||||
.iter()
|
|
||||||
.map(ToString::to_string)
|
|
||||||
.collect(),
|
|
||||||
|
|
||||||
nonce_range: constants::GET_BLOCK_TEMPLATE_NONCE_RANGE_FIELD.to_string(),
|
nonce_range: constants::GET_BLOCK_TEMPLATE_NONCE_RANGE_FIELD.to_string(),
|
||||||
|
|
||||||
|
@ -361,11 +391,15 @@ where
|
||||||
|
|
||||||
size_limit: MAX_BLOCK_BYTES,
|
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()
|
.boxed()
|
||||||
|
|
|
@ -18,6 +18,8 @@ pub struct GetBlockTemplate {
|
||||||
/// - `proposal`: <https://en.bitcoin.it/wiki/BIP_0023#Block_Proposal>
|
/// - `proposal`: <https://en.bitcoin.it/wiki/BIP_0023#Block_Proposal>
|
||||||
/// - `longpoll`: <https://en.bitcoin.it/wiki/BIP_0022#Optional:_Long_Polling>
|
/// - `longpoll`: <https://en.bitcoin.it/wiki/BIP_0022#Optional:_Long_Polling>
|
||||||
/// - `serverlist`: <https://en.bitcoin.it/wiki/BIP_0023#Logical_Services>
|
/// - `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>,
|
pub capabilities: Vec<String>,
|
||||||
|
|
||||||
/// The version of the block format.
|
/// The version of the block format.
|
||||||
|
@ -65,14 +67,17 @@ pub struct GetBlockTemplate {
|
||||||
#[serde(rename = "coinbasetxn")]
|
#[serde(rename = "coinbasetxn")]
|
||||||
pub coinbase_txn: TransactionTemplate<amount::NegativeOrZero>,
|
pub coinbase_txn: TransactionTemplate<amount::NegativeOrZero>,
|
||||||
|
|
||||||
/// Add documentation.
|
/// 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,
|
||||||
|
|
||||||
/// 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")]
|
#[serde(rename = "mintime")]
|
||||||
// TODO: use DateTime32 type?
|
// TODO: use DateTime32 type?
|
||||||
pub min_time: u32,
|
pub min_time: i64,
|
||||||
|
|
||||||
/// Hardcoded list of block fields the miner is allowed to change.
|
/// Hardcoded list of block fields the miner is allowed to change.
|
||||||
pub mutable: Vec<String>,
|
pub mutable: Vec<String>,
|
||||||
|
@ -89,16 +94,29 @@ pub struct GetBlockTemplate {
|
||||||
#[serde(rename = "sizelimit")]
|
#[serde(rename = "sizelimit")]
|
||||||
pub size_limit: u64,
|
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?
|
// TODO: use DateTime32 type?
|
||||||
#[serde(rename = "curtime")]
|
#[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.
|
// TODO: use CompactDifficulty type.
|
||||||
pub bits: String,
|
pub bits: String,
|
||||||
|
|
||||||
/// Add documentation.
|
/// The height of the next block in the best chain.
|
||||||
// TODO: use Height type?
|
// TODO: use Height type?
|
||||||
pub height: u32,
|
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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,18 +5,24 @@
|
||||||
//! cargo insta test --review --features getblocktemplate-rpcs --delete-unreferenced-snapshots
|
//! cargo insta test --review --features getblocktemplate-rpcs --delete-unreferenced-snapshots
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
|
use chrono::{TimeZone, Utc};
|
||||||
|
use hex::FromHex;
|
||||||
use insta::Settings;
|
use insta::Settings;
|
||||||
use tower::{buffer::Buffer, Service};
|
use tower::{buffer::Buffer, Service};
|
||||||
|
|
||||||
use zebra_chain::{
|
use zebra_chain::{
|
||||||
|
block::Hash,
|
||||||
chain_tip::mock::MockChainTip,
|
chain_tip::mock::MockChainTip,
|
||||||
parameters::{Network, NetworkUpgrade},
|
parameters::{Network, NetworkUpgrade},
|
||||||
serialization::ZcashDeserializeInto,
|
serialization::ZcashDeserializeInto,
|
||||||
transaction::Transaction,
|
transaction::Transaction,
|
||||||
transparent,
|
transparent,
|
||||||
|
work::difficulty::{CompactDifficulty, ExpandedDifficulty, U256},
|
||||||
};
|
};
|
||||||
use zebra_node_services::mempool;
|
use zebra_node_services::mempool;
|
||||||
|
|
||||||
|
use zebra_state::{GetBlockTemplateChainInfo, ReadRequest, ReadResponse};
|
||||||
|
|
||||||
use zebra_test::mock_service::{MockService, PanicAssertion};
|
use zebra_test::mock_service::{MockService, PanicAssertion};
|
||||||
|
|
||||||
use crate::methods::{
|
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])),
|
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();
|
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));
|
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(
|
let get_block_template_rpc = GetBlockTemplateRpcImpl::new(
|
||||||
network,
|
network,
|
||||||
mining_config,
|
mining_config.clone(),
|
||||||
Buffer::new(mempool.clone(), 1),
|
Buffer::new(mempool.clone(), 1),
|
||||||
read_state,
|
read_state,
|
||||||
mock_chain_tip,
|
mock_chain_tip.clone(),
|
||||||
chain_verifier,
|
chain_verifier.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// `getblockcount`
|
// `getblockcount`
|
||||||
|
@ -103,7 +124,39 @@ pub async fn test_responses<State, ReadState>(
|
||||||
|
|
||||||
snapshot_rpc_getblockhash(get_block_hash, &settings);
|
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`
|
// `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());
|
let get_block_template = tokio::spawn(get_block_template_rpc.get_block_template());
|
||||||
|
|
||||||
mempool
|
mempool
|
||||||
|
|
|
@ -5,7 +5,7 @@ expression: block_template
|
||||||
{
|
{
|
||||||
"capabilities": [],
|
"capabilities": [],
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"previousblockhash": "0000000000000000000000000000000000000000000000000000000000000000",
|
"previousblockhash": "0000000000d723156d9b65ffcf4984da7a19675ed7e2f06d9e5d5188af087bf8",
|
||||||
"blockcommitmentshash": "0000000000000000000000000000000000000000000000000000000000000000",
|
"blockcommitmentshash": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||||
"lightclientroothash": "0000000000000000000000000000000000000000000000000000000000000000",
|
"lightclientroothash": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||||
"finalsaplingroothash": "0000000000000000000000000000000000000000000000000000000000000000",
|
"finalsaplingroothash": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||||
|
@ -25,8 +25,8 @@ expression: block_template
|
||||||
"sigops": 0,
|
"sigops": 0,
|
||||||
"required": true
|
"required": true
|
||||||
},
|
},
|
||||||
"target": "",
|
"target": "0000000000000000000000000000000000000000000000000000000000000001",
|
||||||
"mintime": 0,
|
"mintime": 1654008606,
|
||||||
"mutable": [
|
"mutable": [
|
||||||
"time",
|
"time",
|
||||||
"transactions",
|
"transactions",
|
||||||
|
@ -35,7 +35,8 @@ expression: block_template
|
||||||
"noncerange": "00000000ffffffff",
|
"noncerange": "00000000ffffffff",
|
||||||
"sigoplimit": 20000,
|
"sigoplimit": 20000,
|
||||||
"sizelimit": 2000000,
|
"sizelimit": 2000000,
|
||||||
"curtime": 0,
|
"curtime": 1654008617,
|
||||||
"bits": "",
|
"bits": "01010000",
|
||||||
"height": 0
|
"height": 1687105,
|
||||||
|
"maxtime": 1654008728
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ expression: block_template
|
||||||
{
|
{
|
||||||
"capabilities": [],
|
"capabilities": [],
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"previousblockhash": "0000000000000000000000000000000000000000000000000000000000000000",
|
"previousblockhash": "0000000000d723156d9b65ffcf4984da7a19675ed7e2f06d9e5d5188af087bf8",
|
||||||
"blockcommitmentshash": "0000000000000000000000000000000000000000000000000000000000000000",
|
"blockcommitmentshash": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||||
"lightclientroothash": "0000000000000000000000000000000000000000000000000000000000000000",
|
"lightclientroothash": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||||
"finalsaplingroothash": "0000000000000000000000000000000000000000000000000000000000000000",
|
"finalsaplingroothash": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||||
|
@ -25,8 +25,8 @@ expression: block_template
|
||||||
"sigops": 0,
|
"sigops": 0,
|
||||||
"required": true
|
"required": true
|
||||||
},
|
},
|
||||||
"target": "",
|
"target": "0000000000000000000000000000000000000000000000000000000000000001",
|
||||||
"mintime": 0,
|
"mintime": 1654008606,
|
||||||
"mutable": [
|
"mutable": [
|
||||||
"time",
|
"time",
|
||||||
"transactions",
|
"transactions",
|
||||||
|
@ -35,7 +35,8 @@ expression: block_template
|
||||||
"noncerange": "00000000ffffffff",
|
"noncerange": "00000000ffffffff",
|
||||||
"sigoplimit": 20000,
|
"sigoplimit": 20000,
|
||||||
"sizelimit": 2000000,
|
"sizelimit": 2000000,
|
||||||
"curtime": 0,
|
"curtime": 1654008617,
|
||||||
"bits": "",
|
"bits": "01010000",
|
||||||
"height": 0
|
"height": 1842421,
|
||||||
|
"maxtime": 1654008728
|
||||||
}
|
}
|
||||||
|
|
|
@ -787,48 +787,47 @@ async fn rpc_getblockhash() {
|
||||||
async fn rpc_getblocktemplate() {
|
async fn rpc_getblocktemplate() {
|
||||||
use std::panic;
|
use std::panic;
|
||||||
|
|
||||||
|
use chrono::{TimeZone, Utc};
|
||||||
|
|
||||||
use crate::methods::get_block_template_rpcs::constants::{
|
use crate::methods::get_block_template_rpcs::constants::{
|
||||||
GET_BLOCK_TEMPLATE_MUTABLE_FIELD, GET_BLOCK_TEMPLATE_NONCE_RANGE_FIELD,
|
GET_BLOCK_TEMPLATE_MUTABLE_FIELD, GET_BLOCK_TEMPLATE_NONCE_RANGE_FIELD,
|
||||||
};
|
};
|
||||||
use zebra_chain::{
|
use zebra_chain::{
|
||||||
amount::{Amount, NonNegative},
|
amount::{Amount, NonNegative},
|
||||||
block::{MAX_BLOCK_BYTES, ZCASH_BLOCK_VERSION},
|
block::{Hash, MAX_BLOCK_BYTES, ZCASH_BLOCK_VERSION},
|
||||||
chain_tip::mock::MockChainTip,
|
chain_tip::mock::MockChainTip,
|
||||||
|
work::difficulty::{CompactDifficulty, ExpandedDifficulty, U256},
|
||||||
};
|
};
|
||||||
use zebra_consensus::MAX_BLOCK_SIGOPS;
|
use zebra_consensus::MAX_BLOCK_SIGOPS;
|
||||||
|
|
||||||
|
use zebra_state::{GetBlockTemplateChainInfo, ReadRequest, ReadResponse};
|
||||||
|
|
||||||
let _init_guard = zebra_test::init();
|
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();
|
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 (
|
let read_state = MockService::build().for_unit_tests();
|
||||||
chain_verifier,
|
let chain_verifier = MockService::build().for_unit_tests();
|
||||||
_transaction_verifier,
|
|
||||||
_parameter_download_task_handle,
|
|
||||||
_max_checkpoint_height,
|
|
||||||
) = zebra_consensus::chain::init(
|
|
||||||
zebra_consensus::Config::default(),
|
|
||||||
Mainnet,
|
|
||||||
state.clone(),
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let mining_config = get_block_template_rpcs::config::Config {
|
let mining_config = get_block_template_rpcs::config::Config {
|
||||||
miner_address: Some(transparent::Address::from_script_hash(Mainnet, [0x7e; 20])),
|
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();
|
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));
|
mock_chain_tip_sender.send_estimated_distance_to_network_chain_tip(Some(0));
|
||||||
|
|
||||||
// Init RPC
|
// Init RPC
|
||||||
|
@ -836,11 +835,26 @@ async fn rpc_getblocktemplate() {
|
||||||
Mainnet,
|
Mainnet,
|
||||||
mining_config,
|
mining_config,
|
||||||
Buffer::new(mempool.clone(), 1),
|
Buffer::new(mempool.clone(), 1),
|
||||||
read_state,
|
read_state.clone(),
|
||||||
mock_chain_tip,
|
mock_chain_tip,
|
||||||
tower::ServiceBuilder::new().service(chain_verifier),
|
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());
|
let get_block_template = tokio::spawn(get_block_template_rpc.get_block_template());
|
||||||
|
|
||||||
mempool
|
mempool
|
||||||
|
@ -858,11 +872,14 @@ async fn rpc_getblocktemplate() {
|
||||||
})
|
})
|
||||||
.expect("unexpected error in getblocktemplate RPC call");
|
.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_eq!(get_block_template.version, ZCASH_BLOCK_VERSION);
|
||||||
assert!(get_block_template.transactions.is_empty());
|
assert!(get_block_template.transactions.is_empty());
|
||||||
assert!(get_block_template.target.is_empty());
|
assert_eq!(
|
||||||
assert_eq!(get_block_template.min_time, 0);
|
get_block_template.target,
|
||||||
|
"0000000000000000000000000000000000000000000000000000000000000001"
|
||||||
|
);
|
||||||
|
assert_eq!(get_block_template.min_time, fake_min_time.timestamp());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
get_block_template.mutable,
|
get_block_template.mutable,
|
||||||
GET_BLOCK_TEMPLATE_MUTABLE_FIELD.to_vec()
|
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.sigop_limit, MAX_BLOCK_SIGOPS);
|
||||||
assert_eq!(get_block_template.size_limit, MAX_BLOCK_BYTES);
|
assert_eq!(get_block_template.size_limit, MAX_BLOCK_BYTES);
|
||||||
assert_eq!(get_block_template.cur_time, 0);
|
assert_eq!(get_block_template.cur_time, fake_cur_time.timestamp());
|
||||||
assert!(get_block_template.bits.is_empty());
|
assert_eq!(get_block_template.bits, "01010000");
|
||||||
assert_eq!(get_block_template.height, 0);
|
assert_eq!(get_block_template.height, 1687105); // nu5 height
|
||||||
|
assert_eq!(get_block_template.max_time, fake_max_time.timestamp());
|
||||||
|
|
||||||
// Coinbase transaction checks.
|
// Coinbase transaction checks.
|
||||||
assert!(get_block_template.coinbase_txn.required);
|
assert!(get_block_template.coinbase_txn.required);
|
||||||
|
|
|
@ -32,6 +32,8 @@ pub use config::{check_and_delete_old_databases, Config};
|
||||||
pub use constants::MAX_BLOCK_REORG_HEIGHT;
|
pub use constants::MAX_BLOCK_REORG_HEIGHT;
|
||||||
pub use error::{BoxError, CloneError, CommitBlockError, ValidateContextError};
|
pub use error::{BoxError, CloneError, CommitBlockError, ValidateContextError};
|
||||||
pub use request::{FinalizedBlock, HashOrHeight, PreparedBlock, ReadRequest, Request};
|
pub use request::{FinalizedBlock, HashOrHeight, PreparedBlock, ReadRequest, Request};
|
||||||
|
#[cfg(feature = "getblocktemplate-rpcs")]
|
||||||
|
pub use response::GetBlockTemplateChainInfo;
|
||||||
pub use response::{ReadResponse, Response};
|
pub use response::{ReadResponse, Response};
|
||||||
pub use service::{
|
pub use service::{
|
||||||
chain_tip::{ChainTipChange, LatestChainTip, TipAction},
|
chain_tip::{ChainTipChange, LatestChainTip, TipAction},
|
||||||
|
|
|
@ -744,6 +744,14 @@ pub enum ReadRequest {
|
||||||
/// * [`ReadResponse::BlockHash(Some(hash))`](ReadResponse::BlockHash) if the block is in the best chain;
|
/// * [`ReadResponse::BlockHash(Some(hash))`](ReadResponse::BlockHash) if the block is in the best chain;
|
||||||
/// * [`ReadResponse::BlockHash(None)`](ReadResponse::BlockHash) otherwise.
|
/// * [`ReadResponse::BlockHash(None)`](ReadResponse::BlockHash) otherwise.
|
||||||
BestChainBlockHash(block::Height),
|
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 {
|
impl ReadRequest {
|
||||||
|
@ -766,6 +774,8 @@ impl ReadRequest {
|
||||||
ReadRequest::UtxosByAddresses(_) => "utxos_by_addesses",
|
ReadRequest::UtxosByAddresses(_) => "utxos_by_addesses",
|
||||||
#[cfg(feature = "getblocktemplate-rpcs")]
|
#[cfg(feature = "getblocktemplate-rpcs")]
|
||||||
ReadRequest::BestChainBlockHash(_) => "best_chain_block_hash",
|
ReadRequest::BestChainBlockHash(_) => "best_chain_block_hash",
|
||||||
|
#[cfg(feature = "getblocktemplate-rpcs")]
|
||||||
|
ReadRequest::ChainInfo => "chain_info",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,9 @@ use zebra_chain::{
|
||||||
transparent,
|
transparent,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "getblocktemplate-rpcs")]
|
||||||
|
use zebra_chain::work::difficulty::CompactDifficulty;
|
||||||
|
|
||||||
// Allow *only* these unused imports, so that rustdoc link resolution
|
// Allow *only* these unused imports, so that rustdoc link resolution
|
||||||
// will work with inline links.
|
// will work with inline links.
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
|
@ -115,6 +118,32 @@ pub enum ReadResponse {
|
||||||
/// Response to [`ReadRequest::BestChainBlockHash`](crate::ReadRequest::BestChainBlockHash) with the
|
/// Response to [`ReadRequest::BestChainBlockHash`](crate::ReadRequest::BestChainBlockHash) with the
|
||||||
/// specified block hash.
|
/// specified block hash.
|
||||||
BlockHash(Option<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.
|
/// Conversion from read-only [`ReadResponse`]s to read-write [`Response`]s.
|
||||||
|
@ -155,6 +184,10 @@ impl TryFrom<ReadResponse> for Response {
|
||||||
ReadResponse::BlockHash(_) => {
|
ReadResponse::BlockHash(_) => {
|
||||||
Err("there is no corresponding Response for this ReadResponse")
|
Err("there is no corresponding Response for this ReadResponse")
|
||||||
}
|
}
|
||||||
|
#[cfg(feature = "getblocktemplate-rpcs")]
|
||||||
|
ReadResponse::ChainInfo(_) => {
|
||||||
|
Err("there is no corresponding Response for this ReadResponse")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1522,13 +1522,6 @@ impl Service<ReadRequest> for ReadStateService {
|
||||||
// Used by get_block_hash RPC.
|
// Used by get_block_hash RPC.
|
||||||
#[cfg(feature = "getblocktemplate-rpcs")]
|
#[cfg(feature = "getblocktemplate-rpcs")]
|
||||||
ReadRequest::BestChainBlockHash(height) => {
|
ReadRequest::BestChainBlockHash(height) => {
|
||||||
metrics::counter!(
|
|
||||||
"state.requests",
|
|
||||||
1,
|
|
||||||
"service" => "read_state",
|
|
||||||
"type" => "best_chain_block_hash",
|
|
||||||
);
|
|
||||||
|
|
||||||
let timer = CodeTimer::start();
|
let timer = CodeTimer::start();
|
||||||
|
|
||||||
let state = self.clone();
|
let state = self.clone();
|
||||||
|
@ -1554,6 +1547,53 @@ impl Service<ReadRequest> for ReadStateService {
|
||||||
.map(|join_result| join_result.expect("panic in ReadRequest::BestChainBlockHash"))
|
.map(|join_result| join_result.expect("panic in ReadRequest::BestChainBlockHash"))
|
||||||
.boxed()
|
.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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,7 @@ pub(crate) mod utxo;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
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
|
/// Check that the `prepared` block is contextually valid for `network`, based
|
||||||
/// on the `finalized_tip_height` and `relevant_chain`.
|
/// on the `finalized_tip_height` and `relevant_chain`.
|
||||||
|
|
|
@ -46,7 +46,7 @@ pub const POW_MAX_ADJUST_DOWN_PERCENT: i32 = 32;
|
||||||
pub const BLOCK_MAX_TIME_SINCE_MEDIAN: i64 = 90 * 60;
|
pub const BLOCK_MAX_TIME_SINCE_MEDIAN: i64 = 90 * 60;
|
||||||
|
|
||||||
/// Contains the context needed to calculate the adjusted difficulty for a block.
|
/// 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
|
/// The `header.time` field from the candidate block
|
||||||
candidate_time: DateTime<Utc>,
|
candidate_time: DateTime<Utc>,
|
||||||
/// The coinbase height from the candidate block
|
/// The coinbase height from the candidate block
|
||||||
|
@ -99,8 +99,8 @@ impl AdjustedDifficulty {
|
||||||
let previous_block_height = (candidate_block_height - 1)
|
let previous_block_height = (candidate_block_height - 1)
|
||||||
.expect("contextual validation is never run on the genesis block");
|
.expect("contextual validation is never run on the genesis block");
|
||||||
|
|
||||||
AdjustedDifficulty::new_from_header(
|
AdjustedDifficulty::new_from_header_time(
|
||||||
&candidate_block.header,
|
candidate_block.header.time,
|
||||||
previous_block_height,
|
previous_block_height,
|
||||||
network,
|
network,
|
||||||
context,
|
context,
|
||||||
|
@ -108,7 +108,7 @@ impl AdjustedDifficulty {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialise and return a new [`AdjustedDifficulty`] using a
|
/// 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
|
/// Designed for use when validating block headers, where the full block has not
|
||||||
/// been downloaded yet.
|
/// been downloaded yet.
|
||||||
|
@ -118,8 +118,8 @@ impl AdjustedDifficulty {
|
||||||
/// # Panics
|
/// # Panics
|
||||||
///
|
///
|
||||||
/// If the context contains fewer than 28 items.
|
/// If the context contains fewer than 28 items.
|
||||||
pub fn new_from_header<C>(
|
pub fn new_from_header_time<C>(
|
||||||
candidate_header: &block::Header,
|
candidate_header_time: DateTime<Utc>,
|
||||||
previous_block_height: block::Height,
|
previous_block_height: block::Height,
|
||||||
network: Network,
|
network: Network,
|
||||||
context: C,
|
context: C,
|
||||||
|
@ -142,7 +142,7 @@ impl AdjustedDifficulty {
|
||||||
.expect("not enough context: difficulty adjustment needs at least 28 (PoWAveragingWindow + PoWMedianBlockSpan) headers");
|
.expect("not enough context: difficulty adjustment needs at least 28 (PoWAveragingWindow + PoWMedianBlockSpan) headers");
|
||||||
|
|
||||||
AdjustedDifficulty {
|
AdjustedDifficulty {
|
||||||
candidate_time: candidate_header.time,
|
candidate_time: candidate_header_time,
|
||||||
candidate_height,
|
candidate_height,
|
||||||
network,
|
network,
|
||||||
relevant_difficulty_thresholds,
|
relevant_difficulty_thresholds,
|
||||||
|
|
|
@ -16,6 +16,8 @@ use crate::service;
|
||||||
|
|
||||||
pub mod address;
|
pub mod address;
|
||||||
pub mod block;
|
pub mod block;
|
||||||
|
#[cfg(feature = "getblocktemplate-rpcs")]
|
||||||
|
pub mod difficulty;
|
||||||
pub mod find;
|
pub mod find;
|
||||||
pub mod tree;
|
pub mod tree;
|
||||||
|
|
||||||
|
@ -33,7 +35,7 @@ pub use block::{
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "getblocktemplate-rpcs")]
|
#[cfg(feature = "getblocktemplate-rpcs")]
|
||||||
pub use block::hash;
|
pub use {block::hash, difficulty::difficulty_and_time_info};
|
||||||
|
|
||||||
pub use find::{
|
pub use find::{
|
||||||
best_tip, block_locator, chain_contains_hash, depth, find_chain_hashes, find_chain_headers,
|
best_tip, block_locator, chain_contains_hash, depth, find_chain_hashes, find_chain_headers,
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue