zebra/zebra-consensus/src/script.rs

147 lines
5.5 KiB
Rust
Raw Normal View History

use std::{collections::HashMap, future::Future, pin::Pin, sync::Arc};
use tower::timeout::Timeout;
use tracing::Instrument;
use zebra_chain::{parameters::NetworkUpgrade, transaction::Transaction, transparent};
use zebra_state::Utxo;
use crate::BoxError;
/// A timeout applied to UTXO lookup requests.
///
/// The exact value is non-essential, but this should be long enough to allow
/// out-of-order verification of blocks (UTXOs are not required to be ready
Decrease the UTXO request timeout (#1407) UTXO requests during transaction input verification can time out because: 1. The block that creates the UTXO is queued for download or verify, but it hasn't been committed yet. The creating block might spend UTXOs that come from other recent blocks, so UTXO verification can depend on a (non-contiguous) sequence of block verifications. In this case, Zebra should wait for additional block download and verify tasks to complete. 2. The block that creates the UTXO isn't queued for download. This can happen because the block is gossiped block that's much higher than the current tip, or because a peer sent the syncer a bad list of block hashes. In this case, Zebra should discard the timed out block, and restart the sync. We need to choose a timeout that balances these two cases, so we time out after 180 seconds. Assuming Zebra can download at least 1 MB per second, 180 seconds is enough time to download a few hundred blocks. So Zebra should be able to download and verify the next block before the UTXOs that it creates time out. (Since Zebra has already verified all the blocks before the next block, its UTXO requests should return immediately.) Even if some peers time out downloads, a block can only be pending download for 80 seconds (4 retries * 20 second timeout) before the download fails. So the UTXO timeout doesn't need to be much larger than this overall download timeout - because the download timeout will happen first on slow networks. Alternately, if the download for the creating block was never queued, Zebra should timeout as soon as possible - so it can restart the sync and download the creating block. As a side-effect, a lower UTXO timeout also makes it slightly easier to debug UTXO issues, because unsatisfiable queries fail faster.
2020-11-30 16:12:25 -08:00
/// immediately) while being short enough to:
/// * prune blocks that are too far in the future to be worth keeping in the
/// queue,
/// * fail blocks that reference invalid UTXOs, and
/// * fail blocks that reference UTXOs from blocks that have temporarily failed
/// to download, because a peer sent Zebra a bad list of block hashes. (The
/// UTXO verification failure will restart the sync, and re-download the
/// chain in the correct order.)
const UTXO_LOOKUP_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(3 * 60);
/// Asynchronous script verification.
///
/// The verifier asynchronously requests the UTXO a transaction attempts
/// to use as an input, and verifies the script as soon as it becomes
/// available. This allows script verification to be performed
/// asynchronously, rather than requiring that the entire chain up to
/// the previous block is ready.
///
/// The asynchronous script verification design is documented in [RFC4].
///
/// [RFC4]: https://zebra.zfnd.org/dev/rfcs/0004-asynchronous-script-verification.html
#[derive(Debug, Clone)]
pub struct Verifier<ZS> {
state: Timeout<ZS>,
}
impl<ZS> Verifier<ZS> {
pub fn new(state: ZS) -> Self {
Self {
state: Timeout::new(state, UTXO_LOOKUP_TIMEOUT),
}
}
}
/// A script verification request.
#[derive(Debug)]
pub struct Request {
/// Ideally, this would supply only an `Outpoint` and the unlock script,
/// rather than the entire `Transaction`, but we call a C++
/// implementation, and its FFI requires the entire transaction.
///
/// This causes quadratic script verification behavior, so
/// at some future point, we need to reform this data.
pub transaction: Arc<Transaction>,
pub input_index: usize,
/// A set of additional UTXOs known in the context of this verification request.
///
/// This allows specifying additional UTXOs that are not already known to the chain state.
pub known_utxos: Arc<HashMap<transparent::OutPoint, Utxo>>,
/// The network upgrade active in the context of this verification request.
///
/// Because the consensus branch ID changes with each network upgrade,
/// it has to be specified on a per-request basis.
pub upgrade: NetworkUpgrade,
}
impl<ZS> tower::Service<Request> for Verifier<ZS>
where
ZS: tower::Service<zebra_state::Request, Response = zebra_state::Response, Error = BoxError>,
ZS::Future: Send + 'static,
{
type Response = ();
type Error = BoxError;
type Future =
Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>>;
fn poll_ready(
&mut self,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<(), Self::Error>> {
self.state.poll_ready(cx)
}
fn call(&mut self, req: Request) -> Self::Future {
use futures_util::FutureExt;
let Request {
transaction,
input_index,
known_utxos,
upgrade,
} = req;
let input = &transaction.inputs()[input_index];
let branch_id = upgrade
.branch_id()
.expect("post-Sapling NUs have a consensus branch ID");
match input {
transparent::Input::PrevOut { outpoint, .. } => {
let outpoint = *outpoint;
let span = tracing::trace_span!("script", ?outpoint);
let query =
span.in_scope(|| self.state.call(zebra_state::Request::AwaitUtxo(outpoint)));
async move {
tracing::trace!("awaiting outpoint lookup");
let utxo = if let Some(output) = known_utxos.get(&outpoint) {
tracing::trace!("UXTO in known_utxos, discarding query");
output.clone()
} else if let zebra_state::Response::Utxo(utxo) = query.await? {
utxo
} else {
unreachable!("AwaitUtxo always responds with Utxo")
};
tracing::trace!(?utxo, "got UTXO");
if transaction.inputs().len() < 20 {
zebra_script::is_valid(
transaction,
branch_id,
(input_index as u32, utxo.output),
)?;
tracing::trace!("script verification succeeded");
} else {
tracing::debug!(
inputs.len = transaction.inputs().len(),
"skipping verification of script with many inputs to avoid quadratic work until we fix zebra_script/zcash_script interface"
);
}
Ok(())
}
.instrument(span)
.boxed()
}
transparent::Input::Coinbase { .. } => {
async { Err("unexpected coinbase input".into()) }.boxed()
}
}
}
}