Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

change(rpc): check contextual validity of getblocktemplate response #5630

Closed
wants to merge 12 commits into from
7 changes: 7 additions & 0 deletions zebra-chain/src/work/equihash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ pub(crate) const SOLUTION_SIZE: usize = 1344;
#[derive(Deserialize, Serialize)]
pub struct Solution(#[serde(with = "BigArray")] pub [u8; SOLUTION_SIZE]);

#[cfg(feature = "getblocktemplate-rpcs")]
impl Default for Solution {
fn default() -> Self {
Self([0; SOLUTION_SIZE])
}
}

impl Solution {
/// The length of the portion of the header used as input when verifying
/// equihash solutions, in bytes.
Expand Down
57 changes: 55 additions & 2 deletions zebra-rpc/src/methods/get_block_template_rpcs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ where
let network = self.network;
let miner_address = self.miner_address;

let mut state = self.state.clone();
let mempool = self.mempool.clone();
let latest_chain_tip = self.latest_chain_tip.clone();

Expand All @@ -294,11 +295,18 @@ where
let (merkle_root, auth_data_root) =
calculate_transaction_roots(&coinbase_tx, &mempool_txs);

let prepared_block_txs: Vec<_> = mempool_txs
.iter()
.map(|tx| tx.transaction.transaction.clone())
.chain(iter::once(coinbase_tx.transaction.clone()))
.collect();

// Convert into TransactionTemplates
let mempool_txs = mempool_txs.iter().map(Into::into).collect();

let empty_string = String::from("");
Ok(GetBlockTemplate {

let get_block_template = GetBlockTemplate {
capabilities: vec![],

version: ZCASH_BLOCK_VERSION,
Expand Down Expand Up @@ -338,7 +346,28 @@ where
bits: empty_string,

height: 0,
})
};

let request = zebra_state::ReadRequest::CheckContextualValidity(
make_test_prepared_block(&get_block_template, prepared_block_txs),
);

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,
})?;

match response {
zebra_state::ReadResponse::Validated => Ok(()),
_ => unreachable!("unmatched response to a CheckContextualValidity request"),
}?;

Ok(get_block_template)
}
.boxed()
}
Expand Down Expand Up @@ -558,3 +587,27 @@ fn get_height_from_int(index: i32, tip_height: Height) -> Result<Height> {
Ok(Height(sanitized_height))
}
}

/// Make a test PreparedBlock the getblocktemplate data to contextually validate
fn make_test_prepared_block(
get_block_template: &GetBlockTemplate,
transactions: Vec<Arc<Transaction>>,
) -> zebra_state::PreparedBlock {
let block = Arc::new(Block {
header: Arc::new(get_block_template.into()),
transactions,
});

let hash = block.hash();
let height = Height(get_block_template.height);
let transaction_hashes: Arc<[_]> = block.transactions.iter().map(|t| t.hash()).collect();
let new_outputs = transparent::new_ordered_outputs(&block, &transaction_hashes);

zebra_state::PreparedBlock {
block,
hash,
height,
new_outputs,
transaction_hashes,
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
//! The `GetBlockTempate` type is the output of the `getblocktemplate` RPC method.

use zebra_chain::{amount, block::ChainHistoryBlockTxAuthCommitmentHash};
use chrono::Utc;
use zebra_chain::{
amount,
block::{ChainHistoryBlockTxAuthCommitmentHash, Header},
parameters::Network,
work::{difficulty::ExpandedDifficulty, equihash::Solution},
};

use crate::methods::{
get_block_template_rpcs::types::{
Expand Down Expand Up @@ -102,3 +108,32 @@ pub struct GetBlockTemplate {
// TODO: use Height type?
pub height: u32,
}

impl From<&GetBlockTemplate> for Header {
fn from(get_block_template: &GetBlockTemplate) -> Self {
let &GetBlockTemplate {
version,
previous_block_hash: GetBlockHash(previous_block_hash),
default_roots:
DefaultRoots {
merkle_root,
block_commitments_hash,
..
},
..
} = get_block_template;

Self {
version,
previous_block_hash,
merkle_root,
commitment_bytes: block_commitments_hash.into(),
time: Utc::now(),
// TODO: get difficulty from target once it uses ExpandedDifficulty type
difficulty_threshold: ExpandedDifficulty::target_difficulty_limit(Network::Mainnet)
.to_compact(),
nonce: [0; 32],
solution: Solution::default(),
}
}
}
35 changes: 12 additions & 23 deletions zebra-rpc/src/methods/tests/vectors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -797,29 +797,9 @@ async fn rpc_getblocktemplate() {

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 mut 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])),
Expand All @@ -833,11 +813,20 @@ 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),
);

tokio::spawn(async move {
read_state
.expect_request_that(|req| {
matches!(req, zebra_state::ReadRequest::CheckContextualValidity(_))
})
.await
.respond(zebra_state::ReadResponse::Validated);
});

let get_block_template = tokio::spawn(get_block_template_rpc.get_block_template());

mempool
Expand Down
8 changes: 8 additions & 0 deletions zebra-state/src/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,12 @@ 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")]
/// Performs contextual validation of the given block
///
/// Returns [`ReadResponse::Validated`] when successful or a validation error
CheckContextualValidity(PreparedBlock),
}

impl ReadRequest {
Expand All @@ -766,6 +772,8 @@ impl ReadRequest {
ReadRequest::UtxosByAddresses(_) => "utxos_by_addesses",
#[cfg(feature = "getblocktemplate-rpcs")]
ReadRequest::BestChainBlockHash(_) => "best_chain_block_hash",
#[cfg(feature = "getblocktemplate-rpcs")]
ReadRequest::CheckContextualValidity(_) => "check_contextual_validity",
}
}

Expand Down
6 changes: 5 additions & 1 deletion zebra-state/src/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ 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::CheckContextualValidity`]
Validated,
}

/// Conversion from read-only [`ReadResponse`]s to read-write [`Response`]s.
Expand Down Expand Up @@ -152,7 +156,7 @@ impl TryFrom<ReadResponse> for Response {
}

#[cfg(feature = "getblocktemplate-rpcs")]
ReadResponse::BlockHash(_) => {
ReadResponse::BlockHash(_) | ReadResponse::Validated => {
Err("there is no corresponding Response for this ReadResponse")
}
}
Expand Down
112 changes: 105 additions & 7 deletions zebra-state/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,84 @@ impl ReadStateService {
fn latest_non_finalized_state(&self) -> NonFinalizedState {
self.non_finalized_state_receiver.cloned_watch_data()
}

/// Check the contextual validity of a block in the best chain
#[cfg(feature = "getblocktemplate-rpcs")]
#[allow(clippy::unwrap_in_result)]
#[tracing::instrument(level = "debug", skip_all)]
fn check_best_chain_contextual_validity(
&self,
block: PreparedBlock,
) -> Result<(), crate::ValidateContextError> {
if let Some(best_chain) = self.latest_non_finalized_state().best_chain() {
let best_chain_latest_non_finalized_state = NonFinalizedState::build(self.network)
.insert_chain(Arc::clone(best_chain))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

qualifying the Arc::clone so it's obvious that nothing's being copied without looking at the type.

.finish();

check::initial_contextual_validity(
self.network,
&self.db,
&best_chain_latest_non_finalized_state,
&block,
)?;

// Reads from disk
let sprout_final_treestates =
check::anchors::fetch_sprout_final_treestates(&self.db, best_chain, &block);

let contextual = best_chain.new_contextually_valid_block(block, &self.db)?;
let mut block_commitment_result = None;
let mut sprout_anchor_result = None;

rayon::in_place_scope_fifo(|scope| {
// Clone function arguments for different threads
let block = Arc::clone(&contextual.block);

scope.spawn_fifo(|_scope| {
block_commitment_result =
Some(check::anchors::sprout_anchors_refer_to_treestates(
sprout_final_treestates,
block,
contextual.height,
contextual.transaction_hashes,
));
});

scope.spawn_fifo(|_scope| {
sprout_anchor_result =
Some(check::block_commitment_is_valid_for_chain_history(
contextual.block,
self.network,
&best_chain.history_tree,
));
});
});

block_commitment_result.expect("scope has finished")?;
sprout_anchor_result.expect("scope has finished")?;
} else {
// Not currently used by the getblocktemplate rpc method because it requires
// a low estimated distance to the network chain tip that means it would return
// an error before reaching this when the non-finalized state is empty anyways
let next_valid_height = self
.db
.finalized_tip_height()
.map(|height| (height + 1).expect("committed heights are valid"))
.unwrap_or(block::Height(0));

if block.height != next_valid_height {
return Err(crate::ValidateContextError::NotReadyToBeCommitted);
}

check::block_commitment_is_valid_for_chain_history(
block.block,
self.network,
&self.db.history_tree(),
)?;
};

Ok(())
}
}

impl Service<Request> for StateService {
Expand Down Expand Up @@ -1522,13 +1600,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();
Expand All @@ -1554,6 +1625,33 @@ 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::CheckContextualValidity(block) => {
let timer = CodeTimer::start();

let state = self.clone();

let span = Span::current();
tokio::task::spawn_blocking(move || {
span.in_scope(move || {
state.check_best_chain_contextual_validity(block)?;
// The work is done in the future.
timer.finish(
module_path!(),
line!(),
"ReadRequest::CheckContextualValidity",
);

Ok(ReadResponse::Validated)
})
})
.map(|join_result| {
join_result.expect("panic in ReadRequest::CheckContextualValidity")
})
.boxed()
}
}
}
}
Expand Down
Loading