diff --git a/grafana/transaction_verification.json b/grafana/transaction_verification.json new file mode 100644 index 00000000000..0e96e030d5c --- /dev/null +++ b/grafana/transaction_verification.json @@ -0,0 +1,207 @@ +{ + "__inputs": [ + { + "name": "DS_PROMETHEUS-ZEBRA", + "label": "Prometheus-Zebra", + "description": "", + "type": "datasource", + "pluginId": "prometheus", + "pluginName": "Prometheus" + } + ], + "__requires": [ + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "8.1.2" + }, + { + "type": "panel", + "id": "graph", + "name": "Graph (old)", + "version": "" + }, + { + "type": "datasource", + "id": "prometheus", + "name": "Prometheus", + "version": "1.0.0" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": null, + "iteration": 1630092146360, + "links": [], + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": null, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 0 + }, + "hiddenSeries": false, + "id": 6, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "8.1.2", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeatDirection": "h", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "rate(gossip_downloaded_transaction_count{job=\"$job\"}[1m]) * 60", + "interval": "", + "legendFormat": "gossip_downloaded_transaction_count per min", + "refId": "C" + }, + { + "exemplar": true, + "expr": "rate(gossip_verified_transaction_count{job=\"$job\"}[1m]) * 60", + "interval": "", + "legendFormat": "gossip_verified_transaction_count per min", + "refId": "D" + }, + { + "exemplar": true, + "expr": "gossip_queued_transaction_count{job=\"$job\"}", + "interval": "", + "legendFormat": "gossip_queued_transaction_count", + "refId": "E" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Transaction Verifier Gossip Count - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "refresh": "5s", + "schemaVersion": 30, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "allValue": null, + "current": {}, + "datasource": "${DS_PROMETHEUS-ZEBRA}", + "definition": "label_values(zcash_chain_verified_block_height, job)", + "description": null, + "error": null, + "hide": 0, + "includeAll": true, + "label": null, + "multi": true, + "name": "job", + "options": [], + "query": { + "query": "label_values(zcash_chain_verified_block_height, job)", + "refId": "StandardVariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + } + ] + }, + "time": { + "from": "now-30m", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "transaction verification", + "uid": "oBEHvS4nz", + "version": 2 +} \ No newline at end of file diff --git a/zebra-consensus/src/error.rs b/zebra-consensus/src/error.rs index 50a1ad7523a..b87f4008793 100644 --- a/zebra-consensus/src/error.rs +++ b/zebra-consensus/src/error.rs @@ -41,6 +41,9 @@ pub enum TransactionError { #[error("coinbase transaction MUST NOT have the EnableSpendsOrchard flag set")] CoinbaseHasEnableSpendsOrchard, + #[error("coinbase transaction MUST NOT exist in mempool")] + CoinbaseInMempool, + #[error("coinbase transaction failed subsidy validation")] Subsidy(#[from] SubsidyError), diff --git a/zebra-consensus/src/transaction.rs b/zebra-consensus/src/transaction.rs index 42b0105fbe5..778af47af2c 100644 --- a/zebra-consensus/src/transaction.rs +++ b/zebra-consensus/src/transaction.rs @@ -168,11 +168,6 @@ where // TODO: break up each chunk into its own method fn call(&mut self, req: Request) -> Self::Future { - if req.is_mempool() { - // XXX determine exactly which rules apply to mempool transactions - unimplemented!("Zebra does not yet have a mempool (#2309)"); - } - let script_verifier = self.script_verifier.clone(); let network = self.network; @@ -187,7 +182,11 @@ where check::has_inputs_and_outputs(&tx)?; if tx.is_coinbase() { - check::coinbase_tx_no_prevout_joinsplit_spend(&tx)?; + if req.is_mempool() { + return Err(TransactionError::CoinbaseInMempool); + } else { + check::coinbase_tx_no_prevout_joinsplit_spend(&tx)?; + } } // [Canopy onward]: `vpub_old` MUST be zero. diff --git a/zebrad/src/components/inbound.rs b/zebrad/src/components/inbound.rs index a3005d2ae90..7dd35f7a36b 100644 --- a/zebrad/src/components/inbound.rs +++ b/zebrad/src/components/inbound.rs @@ -21,6 +21,8 @@ use zebra_consensus::transaction; use zebra_consensus::{chain::VerifyChainError, error::TransactionError}; use zebra_network::AddressBook; +use crate::components::sync::{TRANSACTION_DOWNLOAD_TIMEOUT, TRANSACTION_VERIFY_TIMEOUT}; + // Re-use the syncer timeouts for consistency. use super::mempool::downloads::Downloads as TxDownloads; use super::sync::{BLOCK_DOWNLOAD_TIMEOUT, BLOCK_VERIFY_TIMEOUT}; @@ -36,7 +38,7 @@ type TxVerifier = Buffer< transaction::Request, >; type InboundDownloads = Downloads, Timeout, State>; -type InboundTxDownloads = TxDownloads, Timeout>; +type InboundTxDownloads = TxDownloads, Timeout, State>; pub type NetworkSetupData = (Outbound, Arc>); @@ -179,8 +181,9 @@ impl Service for Inbound { self.state.clone(), )); let tx_downloads = Box::pin(TxDownloads::new( - Timeout::new(outbound, BLOCK_DOWNLOAD_TIMEOUT), - Timeout::new(tx_verifier, BLOCK_VERIFY_TIMEOUT), + Timeout::new(outbound, TRANSACTION_DOWNLOAD_TIMEOUT), + Timeout::new(tx_verifier, TRANSACTION_VERIFY_TIMEOUT), + self.state.clone(), )); result = Ok(()); Setup::Initialized { @@ -340,9 +343,18 @@ impl Service for Inbound { // TODO: send to Tx Download & Verify Stream async { Ok(zn::Response::Nil) }.boxed() } - zn::Request::AdvertiseTransactionIds(_transactions) => { - debug!("ignoring unimplemented request"); - // TODO: send to Tx Download & Verify Stream + zn::Request::AdvertiseTransactionIds(transactions) => { + if let Setup::Initialized { tx_downloads, .. } = &mut self.network_setup { + // TODO: check if we're close to the tip before proceeding? + // what do we do if it's not? + for txid in transactions { + tx_downloads.download_and_verify(txid); + } + } else { + info!( + "ignoring `AdvertiseTransactionIds` request from remote peer during network setup" + ); + } async { Ok(zn::Response::Nil) }.boxed() } zn::Request::AdvertiseBlock(hash) => { diff --git a/zebrad/src/components/mempool/downloads.rs b/zebrad/src/components/mempool/downloads.rs index 5196b37aae9..d823411b3f4 100644 --- a/zebrad/src/components/mempool/downloads.rs +++ b/zebrad/src/components/mempool/downloads.rs @@ -4,6 +4,7 @@ use std::{ task::{Context, Poll}, }; +use color_eyre::eyre::eyre; use futures::{ future::TryFutureExt, ready, @@ -14,9 +15,10 @@ use tokio::{sync::oneshot, task::JoinHandle}; use tower::{Service, ServiceExt}; use tracing_futures::Instrument; -use zebra_chain::{block::Height, transaction::UnminedTxId}; +use zebra_chain::transaction::UnminedTxId; use zebra_consensus::transaction as tx; use zebra_network as zn; +use zebra_state as zs; type BoxError = Box; @@ -30,16 +32,16 @@ type BoxError = Box; /// We use a small concurrency limit, to prevent memory denial-of-service /// attacks. /// -/// The maximum block size is 2 million bytes. A deserialized malicious -/// block with ~225_000 transparent outputs can take up 9MB of RAM. As of +/// The maximum transaction size is 2 million bytes. A deserialized malicious +/// transaction with ~225_000 transparent outputs can take up 9MB of RAM. As of /// February 2021, a growing `Vec` can allocate up to 2x its current length, -/// leading to an overall memory usage of 18MB per malicious block. (See +/// leading to an overall memory usage of 18MB per malicious transaction. (See /// #1880 for more details.) /// -/// Malicious blocks will eventually timeout or fail contextual validation. +/// Malicious transactions will eventually timeout or fail contextual validation. /// Once validation fails, the block is dropped, and its memory is deallocated. /// -/// Since Zebra keeps an `inv` index, inbound downloads for malicious blocks +/// Since Zebra keeps an `inv` index, inbound downloads for malicious transactions /// will be directed to the malicious node that originally gossiped the hash. /// Therefore, this attack can be carried out by a single malicious node. const MAX_INBOUND_CONCURRENCY: usize = 10; @@ -64,12 +66,14 @@ pub enum DownloadAction { /// Represents a [`Stream`] of download and verification tasks. #[pin_project] #[derive(Debug)] -pub struct Downloads +pub struct Downloads where ZN: Service + Send + 'static, ZN::Future: Send, ZV: Service + Send + Clone + 'static, ZV::Future: Send, + ZS: Service + Send + Clone + 'static, + ZS::Future: Send, { // Services /// A service that forwards requests to connected peers, and returns their @@ -79,6 +83,9 @@ where /// A service that verifies downloaded transactions. verifier: ZV, + /// A service that manages cached blockchain state. + state: ZS, + // Internal downloads state /// A list of pending transaction download and verify tasks. #[pin] @@ -89,12 +96,14 @@ where cancel_handles: HashMap>, } -impl Stream for Downloads +impl Stream for Downloads where ZN: Service + Send + Clone + 'static, ZN::Future: Send, ZV: Service + Send + Clone + 'static, ZV::Future: Send, + ZS: Service + Send + Clone + 'static, + ZS::Future: Send, { type Item = Result; @@ -131,12 +140,14 @@ where } } -impl Downloads +impl Downloads where ZN: Service + Send + Clone + 'static, ZN::Future: Send, ZV: Service + Send + Clone + 'static, ZV::Future: Send, + ZS: Service + Send + Clone + 'static, + ZS::Future: Send, { /// Initialize a new download stream with the provided `network` and /// `verifier` services. @@ -144,10 +155,11 @@ where /// The [`Downloads`] stream is agnostic to the network policy, so retry and /// timeout limits should be applied to the `network` service passed into /// this constructor. - pub fn new(network: ZN, verifier: ZV) -> Self { + pub fn new(network: ZN, verifier: ZV, state: ZS) -> Self { Self { network, verifier, + state, pending: FuturesUnordered::new(), cancel_handles: HashMap::new(), } @@ -183,6 +195,7 @@ where let network = self.network.clone(); let verifier = self.verifier.clone(); + let state = self.state.clone(); let fut = async move { // TODO: adapt this for transaction / mempool @@ -196,6 +209,14 @@ where // Err(e) => Err(e), // }?; + let height = match state.oneshot(zs::Request::Tip).await { + Ok(zs::Response::Tip(None)) => Err("no block at the tip".into()), + Ok(zs::Response::Tip(Some((height, _hash)))) => Ok(height), + Ok(_) => unreachable!("wrong response"), + Err(e) => Err(e), + }?; + let height = (height + 1).ok_or_else(|| eyre!("no next height"))?; + let tx = if let zn::Response::Transactions(txs) = network .oneshot(zn::Request::TransactionsById( std::iter::once(txid).collect(), @@ -210,13 +231,16 @@ where }; metrics::counter!("gossip.downloaded.transaction.count", 1); - verifier + let result = verifier .oneshot(tx::Request::Mempool { transaction: tx, - // TODO: pass correct height - height: Height(0), + height, }) - .await + .await; + + tracing::debug!(?txid, ?result, "verified transaction for the mempool"); + + result } .map_ok(|hash| { metrics::counter!("gossip.verified.transaction.count", 1); diff --git a/zebrad/src/components/sync.rs b/zebrad/src/components/sync.rs index 333ff3ff751..849745f8b20 100644 --- a/zebrad/src/components/sync.rs +++ b/zebrad/src/components/sync.rs @@ -116,6 +116,14 @@ pub(super) const BLOCK_DOWNLOAD_TIMEOUT: Duration = Duration::from_secs(15); /// failure loop. pub(super) const BLOCK_VERIFY_TIMEOUT: Duration = Duration::from_secs(180); +/// Controls how long we wait for a transaction download request to complete. +/// TODO: review value and rationale +pub(super) const TRANSACTION_DOWNLOAD_TIMEOUT: Duration = Duration::from_secs(15); + +/// Controls how long we wait for a transaction verify request to complete. +/// TODO: review value and rationale +pub(super) const TRANSACTION_VERIFY_TIMEOUT: Duration = Duration::from_secs(180); + /// Controls how long we wait to restart syncing after finishing a sync run. /// /// This delay should be long enough to: