|
| 1 | +use { |
| 2 | + crate::Responder, |
| 3 | + anyhow::{anyhow, Context}, |
| 4 | + clap::Parser, |
| 5 | + penumbra_asset::Value, |
| 6 | + std::{net::SocketAddr, path::PathBuf}, |
| 7 | + tap::Tap, |
| 8 | +}; |
| 9 | + |
| 10 | +/// Run galileo on an RPC endpoint. |
| 11 | +#[derive(Debug, Clone, Parser)] |
| 12 | +pub struct ServeRpc { |
| 13 | + /// The number of accounts to send funds from. Funds will send from account indices `[0, n-1]`. |
| 14 | + #[clap(long, default_value = "4")] |
| 15 | + account_count: u32, |
| 16 | + /// The path to the directory to use to store data [default: platform appdata directory]. |
| 17 | + #[clap(long, short)] |
| 18 | + data_dir: Option<PathBuf>, |
| 19 | + /// The URL of the pd gRPC endpoint on the remote node. |
| 20 | + /// |
| 21 | + /// This is the node that transactions will be sent to. |
| 22 | + #[clap(short, long, default_value = "https://grpc.testnet.penumbra.zone")] |
| 23 | + node: url::Url, |
| 24 | + /// Bind the gRPC server to this socket. |
| 25 | + /// |
| 26 | + /// The gRPC server supports both grpc (HTTP/2) and grpc-web (HTTP/1.1) clients. |
| 27 | + /// |
| 28 | + /// If `grpc_auto_https` is set, this defaults to `0.0.0.0:443` and uses HTTPS. |
| 29 | + /// |
| 30 | + /// If `grpc_auto_https` is not set, this defaults to `127.0.0.1:8080` without HTTPS. |
| 31 | + #[clap(short, long)] |
| 32 | + grpc_bind: Option<SocketAddr>, |
| 33 | + /// If set, serve gRPC using auto-managed HTTPS with this domain name. |
| 34 | + /// |
| 35 | + /// NOTE: This option automatically provisions TLS certificates from Let's Encrypt and caches |
| 36 | + /// them in the `home` directory. The production LE CA has rate limits, so be careful using |
| 37 | + /// this option. Avoid deleting the certificates and forcing re-issuance, which can lead to |
| 38 | + /// hitting the rate limit. See the `--acme-staging` option. |
| 39 | + #[clap(long, value_name = "DOMAIN")] |
| 40 | + grpc_auto_https: Option<String>, |
| 41 | + /// Enable use of the LetsEncrypt ACME staging API (https://letsencrypt.org/docs/staging-environment/), |
| 42 | + /// which is more forgiving of ratelimits. Set this option to `true` if you're trying out the |
| 43 | + /// `--grpc-auto-https` option for the first time, to validate your configuration, before |
| 44 | + /// subjecting yourself to production ratelimits. |
| 45 | + /// |
| 46 | + /// This option has no effect if `--grpc-auto-https` is not set. |
| 47 | + #[clap(long)] |
| 48 | + acme_staging: bool, |
| 49 | + // Disable transaction sending. Useful for debugging. |
| 50 | + // TODO(kate): wire in dry-run mode. |
| 51 | + // #[clap(long)] |
| 52 | + // dry_run: bool, |
| 53 | + /// The amounts to send for each response, written as typed values 1.87penumbra, 12cubes, etc. |
| 54 | + values: Vec<Value>, |
| 55 | +} |
| 56 | + |
| 57 | +impl ServeRpc { |
| 58 | + /// Run the bot, listening for RPC calls, and responding as appropriate. |
| 59 | + /// |
| 60 | + /// This function should never return, unless an error of some kind is encountered. |
| 61 | + pub async fn exec(self) -> anyhow::Result<()> { |
| 62 | + self.preflight_checks() |
| 63 | + .await |
| 64 | + .context("failed preflight checks")?; |
| 65 | + |
| 66 | + let Self { |
| 67 | + account_count, |
| 68 | + data_dir, |
| 69 | + node, |
| 70 | + grpc_bind, |
| 71 | + grpc_auto_https, |
| 72 | + acme_staging, |
| 73 | + values, |
| 74 | + } = self; |
| 75 | + |
| 76 | + // Use the given `grpc_bind` address if one was specified. If not, we will choose a |
| 77 | + // default depending on whether or not `grpc_auto_https` was set. See the |
| 78 | + // `RootCommand::Start::grpc_bind` documentation above. |
| 79 | + let grpc_bind = { |
| 80 | + use std::net::{IpAddr, Ipv4Addr, SocketAddr}; |
| 81 | + const HTTP_DEFAULT: SocketAddr = |
| 82 | + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080); |
| 83 | + const HTTPS_DEFAULT: SocketAddr = |
| 84 | + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 443); |
| 85 | + let default = || { |
| 86 | + if grpc_auto_https.is_some() { |
| 87 | + HTTPS_DEFAULT |
| 88 | + } else { |
| 89 | + HTTP_DEFAULT |
| 90 | + } |
| 91 | + }; |
| 92 | + grpc_bind.unwrap_or_else(default) |
| 93 | + }; |
| 94 | + |
| 95 | + // Make a worker to handle the address queue. |
| 96 | + let service = |
| 97 | + crate::sender::service::init(data_dir.as_deref(), account_count, &node).await?; |
| 98 | + let (request_tx, responder) = Responder::new(service, 1, values.clone()); |
| 99 | + let (cancel_tx, mut cancel_rx) = tokio::sync::mpsc::channel(1); |
| 100 | + let responder = tokio::spawn(async move { responder.run(cancel_tx).await }); |
| 101 | + |
| 102 | + // Next, create the RPC service. |
| 103 | + let make_svc = crate::rpc::rpc(request_tx)? |
| 104 | + .into_router() |
| 105 | + // Set rather permissive CORS headers for pd's gRPC: the service |
| 106 | + // should be accessible from arbitrary web contexts, such as localhost, |
| 107 | + // or any FQDN that wants to reference its data. |
| 108 | + .layer(tower_http::cors::CorsLayer::permissive()) |
| 109 | + .into_make_service(); |
| 110 | + |
| 111 | + // Now start the GRPC server, initializing an ACME client to use as a certificate |
| 112 | + // resolver if auto-https has been enabled. |
| 113 | + macro_rules! spawn_grpc_server { |
| 114 | + ($server:expr) => { |
| 115 | + tokio::task::spawn($server.serve(make_svc)) |
| 116 | + }; |
| 117 | + } |
| 118 | + let grpc_server = axum_server::bind(grpc_bind); |
| 119 | + let grpc_server = match grpc_auto_https { |
| 120 | + // Auto-https is enabled. Configure the axum accepter, and spawn an ACME worker. |
| 121 | + Some(domain) => { |
| 122 | + let data_dir = // we wouldn't need an error here if we hoist the default up. |
| 123 | + data_dir.ok_or(anyhow!("data directory must be set to use auto-https"))?; |
| 124 | + let (acceptor, acme_worker) = |
| 125 | + penumbra_auto_https::axum_acceptor(data_dir, domain, !acme_staging); |
| 126 | + // TODO(kate): we should eventually propagate errors from the ACME worker task. |
| 127 | + tokio::spawn(acme_worker); |
| 128 | + spawn_grpc_server!(grpc_server.acceptor(acceptor)) |
| 129 | + } |
| 130 | + // Auto-https is not enabled. Spawn only the GRPC server. |
| 131 | + None => { |
| 132 | + spawn_grpc_server!(grpc_server) |
| 133 | + } |
| 134 | + } |
| 135 | + .tap(|_| tracing::info!(address = %grpc_bind, "grpc service is running")); |
| 136 | + |
| 137 | + // Start the RPC server and the responder worker, listening for a cancellation event. |
| 138 | + tokio::select! { |
| 139 | + result = responder => result.map_err(anyhow::Error::from)?.context("error in responder service"), |
| 140 | + result = grpc_server => result.map_err(anyhow::Error::from)?.context("error in grpc service"), |
| 141 | + _ = cancel_rx.recv() => Err(anyhow::anyhow!("cancellation received")), |
| 142 | + } |
| 143 | + } |
| 144 | + |
| 145 | + /// Perform sanity checks on CLI args prior to running. |
| 146 | + async fn preflight_checks(&self) -> anyhow::Result<()> { |
| 147 | + use num_traits::identities::Zero; |
| 148 | + |
| 149 | + // TODO(kate): wire up dry-run mode. |
| 150 | + // if self.dry_run { |
| 151 | + // tracing::info!("dry-run mode is enabled, won't send transactions or post messages"); |
| 152 | + // } |
| 153 | + |
| 154 | + if self.values.is_empty() { |
| 155 | + anyhow::bail!("at least one value must be provided"); |
| 156 | + } else if self.values.iter().any(|v| v.amount.value().is_zero()) { |
| 157 | + anyhow::bail!("all values must be non-zero"); |
| 158 | + } |
| 159 | + |
| 160 | + Ok(()) |
| 161 | + } |
| 162 | +} |
0 commit comments