Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions zcash_local_net/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- **BREAKING: `BlockTemplate` gained a `long_poll_id` field.** The
field deserializes from `getblocktemplate`'s `longpollid`, so any
downstream literal construction of `BlockTemplate` must now populate
it. It feeds the new template prefetch in `Zebrad::generate_blocks`:
after each mined block the harness parks a long-poll
`getblocktemplate` on zebrad, which precomputes the next shielded
coinbase (about two seconds of proof work) while the harness does
other work, and the next `generate_blocks` call consumes the
prefetched template instead of paying that construction inline. The
prefetched template is an empty provisional block, so it is consumed
only when the mempool is empty, the height matches, and the template
carries no transactions; every other case falls back to the previous
fetch-fresh path, which keeps the documented contract that generated
blocks confirm mempool transactions. Measured on zingolib's
`send_shield_cycle` round trip, empty separation blocks drop from
about 2.5 seconds to 0.4–1.6 seconds each. `submit_template_block`
split into the public `fetch_block_template`,
`fetch_block_template_long_poll`, and `submit_block_from_template`
stages, and `mempool_txids` exposes the gate's mempool probe.

### Added

- **Containerized artifacts.** Every managed process (the zebrad
Expand Down
73 changes: 70 additions & 3 deletions zcash_local_net/src/validator/zebrad.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use zingo_test_vectors::{
use std::{net::SocketAddr, path::PathBuf, process::Child};

use crate::rpc_client::RpcRequestClient;
use crate::zebra_rpc::BlockTemplate;
use tempfile::TempDir;

/// Zebrad configuration
Expand Down Expand Up @@ -188,6 +189,10 @@ pub struct Zebrad {
client: RpcRequestClient,
/// Network type
network: NetworkType,
/// In-flight long-poll `getblocktemplate` request spawned after the
/// last mined block, whose response `generate_blocks` may consume as
/// the next block's template when the mempool is empty.
template_prefetch: std::sync::Mutex<Option<tokio::task::JoinHandle<Option<BlockTemplate>>>>,
}

crate::macros::ref_getters!(Zebrad {
Expand Down Expand Up @@ -598,6 +603,7 @@ impl Zebrad {
logs_dir,
data_dir,
client,
template_prefetch: std::sync::Mutex::new(None),
network: config.network_type,
};

Expand Down Expand Up @@ -640,6 +646,55 @@ impl Zebrad {
/// only entry.
const ZEBRAD_RPC_LISTENER_INDEX: usize = 0;

impl Zebrad {
/// Ceiling on waiting for a prefetched template's long poll to resolve, sized well above zebrad's observed ~2s shielded-coinbase precompute so a healthy response is never abandoned while a wedged one cannot stall mining.
const PREFETCH_RESOLUTION_CEILING: std::time::Duration = std::time::Duration::from_secs(10);

/// Spawns a long-poll `getblocktemplate` request presenting `long_poll_id`, storing the task so the next `generate_blocks` call can consume its response, and aborting any stale predecessor.
fn spawn_template_prefetch(&self, long_poll_id: String) {
let client = self.client.clone();
let handle = tokio::spawn(async move {
crate::zebra_rpc::fetch_block_template_long_poll(&client, &long_poll_id)
.await
.ok()
});
if let Some(stale) = self
.template_prefetch
.lock()
.expect("template prefetch lock must not be poisoned")
.replace(handle)
{
stale.abort();
}
}

/// Takes the pending prefetched template when it is safe to mine from it: zebra answers a fired long poll with an empty provisional block, so the template is consumed only when the mempool is empty, it templates exactly `target_height`, and it carries no transactions.
async fn take_prefetched_template(&self, target_height: u32) -> Option<BlockTemplate> {
let mut handle = self
.template_prefetch
.lock()
.expect("template prefetch lock must not be poisoned")
.take()?;
match crate::zebra_rpc::mempool_txids(&self.client).await {
Ok(txids) if txids.is_empty() => {}
_ => {
handle.abort();
return None;
}
}
let template =
match tokio::time::timeout(Self::PREFETCH_RESOLUTION_CEILING, &mut handle).await {
Ok(Ok(Some(template))) => template,
Ok(_) => return None,
Err(_elapsed) => {
handle.abort();
return None;
}
};
(template.height == target_height && template.transactions.is_empty()).then_some(template)
}
}

impl crate::backend::Backend for Zebrad {
fn log_text(&self) -> std::io::Result<String> {
std::fs::read_to_string(self.logs_dir.path().join(crate::logs::STDOUT_LOG))
Expand Down Expand Up @@ -762,17 +817,29 @@ impl Validator for Zebrad {
const ATTEMPT_INTERVAL: std::time::Duration = std::time::Duration::from_millis(100);
for i in 0..n {
let target_height = chain_height + i + 1;
let mut prefetched = self.take_prefetched_template(target_height).await;
let mut last_response = String::new();
let mut advanced = false;
for _ in 0..MAX_ATTEMPTS {
let submission =
crate::zebra_rpc::submit_template_block(&self.client, activation_heights)
let template = match prefetched.take() {
Some(template) => template,
None => crate::zebra_rpc::fetch_block_template(&self.client)
.await
.expect("template block submission should succeed");
.expect("template fetch should succeed"),
};
let long_poll_id = template.long_poll_id.clone();
let submission = crate::zebra_rpc::submit_block_from_template(
&self.client,
&template,
activation_heights,
)
.await
.expect("template block submission should succeed");
last_response = submission.response;

if self.get_chain_height().await >= target_height {
advanced = true;
self.spawn_template_prefetch(long_poll_id);
break;
}
tokio::time::sleep(ATTEMPT_INTERVAL).await;
Expand Down
47 changes: 44 additions & 3 deletions zcash_local_net/src/zebra_rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ pub struct BlockTemplate {
pub coinbase_txn: TransactionTemplate,
/// The non-coinbase transactions.
pub transactions: Vec<TransactionTemplate>,
/// The template's long-poll id, which a follow-up `getblocktemplate` call can present to wait server-side for the next chain state instead of paying template construction inline.
#[serde(rename = "longpollid")]
pub long_poll_id: String,
}

/// The header roots from a template's `defaultroots` field.
Expand Down Expand Up @@ -211,10 +214,39 @@ pub async fn submit_template_block(
client: &crate::rpc_client::RpcRequestClient,
activation_heights: &ActivationHeights,
) -> Result<BlockSubmission, SubmitBlockError> {
let template: BlockTemplate = client
let template = fetch_block_template(client).await?;
submit_block_from_template(client, &template, activation_heights).await
}

/// Fetches a fresh block template for the current chain tip.
pub async fn fetch_block_template(
client: &crate::rpc_client::RpcRequestClient,
) -> Result<BlockTemplate, SubmitBlockError> {
Ok(client
.json_result_from_call("getblocktemplate", "[]".to_string())
.await?;
let block_bytes = proposal_block_bytes(&template, activation_heights)?;
.await?)
}

/// Fetches a block template by presenting `long_poll_id`, so zebrad precomputes the next template's shielded coinbase while it waits for the tip to change instead of building it inline on the next fresh fetch.
pub async fn fetch_block_template_long_poll(
client: &crate::rpc_client::RpcRequestClient,
long_poll_id: &str,
) -> Result<BlockTemplate, SubmitBlockError> {
Ok(client
.json_result_from_call(
"getblocktemplate",
format!(r#"[{{"longpollid":"{long_poll_id}"}}]"#),
)
.await?)
}

/// Assembles `template` into a block proposal and submits it.
pub async fn submit_block_from_template(
client: &crate::rpc_client::RpcRequestClient,
template: &BlockTemplate,
activation_heights: &ActivationHeights,
) -> Result<BlockSubmission, SubmitBlockError> {
let block_bytes = proposal_block_bytes(template, activation_heights)?;
let block_hash = block_hash_hex(&block_bytes);
let block_hex = hex::encode(&block_bytes);
let response = client
Expand All @@ -227,6 +259,15 @@ pub async fn submit_template_block(
})
}

/// Returns the txids currently in zebrad's mempool.
pub async fn mempool_txids(
client: &crate::rpc_client::RpcRequestClient,
) -> Result<Vec<String>, SubmitBlockError> {
Ok(client
.json_result_from_call("getrawmempool", "[]".to_string())
.await?)
}

/// Decode a hex template field, mapping failure to [`ZebraRpcError::InvalidHex`].
fn decode_hex(field: &'static str, hex_str: &str) -> Result<Vec<u8>, ZebraRpcError> {
hex::decode(hex_str).map_err(|e| ZebraRpcError::InvalidHex {
Expand Down
Loading