diff --git a/Cargo.lock b/Cargo.lock index 4769ed4f2..356615726 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13474,6 +13474,7 @@ dependencies = [ "thiserror 2.0.8", "tokio", "tokio-util", + "toml 0.8.19", "tower 0.4.13", "tower-http 0.5.2", "tracing", diff --git a/Cargo.toml b/Cargo.toml index f7fd93e0b..d69a879d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -110,6 +110,7 @@ thiserror = { version = "2.0.3", default-features = false } tokio = "1.37.0" tokio-stream = "0.1.15" tokio-util = "0.7.11" +toml = "0.8.19" tower = "0.4.13" tower-http = "0.5.2" tracing = "0.1.40" diff --git a/docs/src/storage-provider-cli/server.md b/docs/src/storage-provider-cli/server.md index e428af8d4..1a4ae3ab5 100644 --- a/docs/src/storage-provider-cli/server.md +++ b/docs/src/storage-provider-cli/server.md @@ -4,27 +4,6 @@ This chapter covers the available CLI options for the Polka Storage Provider ser -#### `--upload-listen-address` - -The storage server's endpoint address — i.e. where the client will upload their files to. - -It takes in an IP address along with a port in the format: `:`. -Defaults to `127.0.0.1:8001`. - -#### `--rpc-listen-address` - -The RPC server endpoint's address — i.e. where you will submit your deals to. - -It takes in an IP address along with a port in the format: `:`. -Defaults to `127.0.0.1:8000`. - -#### `--node-url` - -The target parachain node's address — i.e. the parachain node the storage provider will submit deals to, etc. - -It takes in an URL, it supports both HTTP and WebSockets and their secure variants. -Defaults to `ws://127.0.0.1:42069`. - ### `--sr25519-key` Sr25519 keypair, encoded as hex, BIP-39 or a dev phrase like `//Alice`. @@ -49,6 +28,27 @@ See [`sp_core::crypto::Pair::from_string_with_seed`](https://docs.rs/sp-core/lat If this `--ed25519-key` is not used, either [`--ecdsa-key`](#--ecdsa-key) or [`--sr25519-key`](#--sr25519-key) MUST be used. +### `--upload-listen-address` + +The storage server's endpoint address — i.e. where the client will upload their files to. + +It takes in an IP address along with a port in the format: `:`. +Defaults to `127.0.0.1:8001`. + +### `--rpc-listen-address` + +The RPC server endpoint's address — i.e. where you will submit your deals to. + +It takes in an IP address along with a port in the format: `:`. +Defaults to `127.0.0.1:8000`. + +### `--node-url` + +The target parachain node's address — i.e. the parachain node the storage provider will submit deals to, etc. + +It takes in an URL, it supports both HTTP and WebSockets and their secure variants. +Defaults to `ws://127.0.0.1:42069`. + ### `--database-directory` The RocksDB storage directory, where deal information will be kept. @@ -72,3 +72,37 @@ The kind of replication proof. Currently, only `StackedDRGWindow2KiBV1P1` is sup ### `--post-proof` The kind of storage proof. Currently, only `StackedDRGWindow2KiBV1P1` is supported to which it defaults. + +### `--porep-parameters` + +The path to the PoRep proving parameters. They are shared across all of the nodes in the network, as the chain stores corresponding Verifying Key parameters. + +### `--post-parameters` + +The path to the PoSt proving parameters. They are shared across all of the nodes in the network, as the chain stores corresponding Verifying Key parameters. + +### `--config` + +Takes in a path to a configuration file, it supports both JSON and TOML (files _must_ have the right extension). +The supported configuration parameters are: + +| Name | Default | +| ----------------------- | ------------------------------ | +| `upload-listen-address` | `127.0.0.1:8000` | +| `rpc-listen-address` | `127.0.0.1:8001` | +| `node-url` | `ws://127.0.0.1:42069` | +| `database-directory` | `/tmp//deals_database` | +| `storage-directory` | `/tmp//deals_storage` | +| `seal-proof` | 2KiB | +| `post-proof` | 2KiB | +| `porep_parameters` | NA | +| `post_parameters` | NA | + +#### Bare bones configuration + +```json +{ + "porep_parameters": "/home/storage_provider/porep.params", + "post_parameters": "/home/storage_provider/post.params", +} +``` diff --git a/examples/start_sp.sh b/examples/start_sp.sh index 73004f5b3..47f48f416 100755 --- a/examples/start_sp.sh +++ b/examples/start_sp.sh @@ -24,4 +24,12 @@ RUST_LOG=debug target/release/storagext-cli --sr25519-key "//Alice" proofs set-p RUST_LOG=debug target/release/storagext-cli --sr25519-key "//Bob" proofs set-post-verifying-key @2KiB.post.vk.scale & wait -RUST_LOG=debug target/release/polka-storage-provider-server --sr25519-key "$PROVIDER" --seal-proof "2KiB" --post-proof "2KiB" --porep-parameters 2KiB.porep.params --post-parameters 2KiB.post.params +echo '{ + "seal_proof": "2KiB", + "post_proof": "2KiB", + "porep_parameters": "2KiB.porep.params", + "post_parameters": "2KiB.post.params" +}' > /tmp/storage_provider.config.json +RUST_LOG=debug target/release/polka-storage-provider-server \ + --sr25519-key "$PROVIDER" \ + --config /tmp/storage_provider.config.json diff --git a/primitives/src/proofs.rs b/primitives/src/proofs.rs index aebef7d55..646ed6b35 100644 --- a/primitives/src/proofs.rs +++ b/primitives/src/proofs.rs @@ -57,7 +57,9 @@ pub enum RegisteredSealProof { impl RegisteredSealProof { pub fn sector_size(&self) -> SectorSize { - SectorSize::_2KiB + match self { + RegisteredSealProof::StackedDRG2KiBV1P1 => SectorSize::_2KiB, + } } /// Produces the windowed PoSt-specific RegisteredProof corresponding @@ -79,6 +81,14 @@ impl RegisteredSealProof { RegisteredSealProof::StackedDRG2KiBV1P1 => 192, } } + + /// Returns [`StackedDRG2KiBV1P1`](RegisteredSealProof::StackedDRG2KiBV1P1). + // NOTE(@jmg-duarte,14/01/2025): wanted to avoid setting a default to use in serde + // this is the alternative + #[allow(non_snake_case)] + pub const fn _2KiB() -> Self { + Self::StackedDRG2KiBV1P1 + } } /// Proof of Spacetime type, indicating version and sector size of the proof. @@ -122,6 +132,14 @@ impl RegisteredPoStProof { RegisteredPoStProof::StackedDRGWindow2KiBV1P1 => 2, } } + + /// Returns [`StackedDRGWindow2KiBV1P1`](RegisteredPoStProof::StackedDRGWindow2KiBV1P1). + // NOTE(@jmg-duarte,14/01/2025): wanted to avoid setting a default to use in serde + // this is the alternative + #[allow(non_snake_case)] + pub const fn _2KiB() -> Self { + Self::StackedDRGWindow2KiBV1P1 + } } // serde_json requires std, hence, to test the serialization, we need: diff --git a/storage-provider/server/Cargo.toml b/storage-provider/server/Cargo.toml index 1801472a0..9409f9afa 100644 --- a/storage-provider/server/Cargo.toml +++ b/storage-provider/server/Cargo.toml @@ -38,12 +38,13 @@ tempfile = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } tokio-util = { workspace = true, features = ["rt"] } +toml = { workspace = true } tower = { workspace = true } tower-http = { workspace = true, features = ["trace"] } tracing = { workspace = true } tracing-appender = { workspace = true } tracing-subscriber = { workspace = true, features = ["env-filter"] } -url = { workspace = true } +url = { workspace = true, features = ["serde"] } uuid = { workspace = true, features = ["v4"] } [lints] diff --git a/storage-provider/server/src/config.rs b/storage-provider/server/src/config.rs new file mode 100644 index 000000000..1e71eceeb --- /dev/null +++ b/storage-provider/server/src/config.rs @@ -0,0 +1,101 @@ +use std::{ + net::{IpAddr, Ipv4Addr, SocketAddr}, + num::NonZero, + path::PathBuf, +}; + +use clap::Args; +use primitives::proofs::{RegisteredPoStProof, RegisteredSealProof}; +use serde::{Deserialize, Serialize}; +use url::Url; + +use crate::DEFAULT_NODE_ADDRESS; + +/// Default address to bind the RPC server to. +const fn default_rpc_listen_address() -> SocketAddr { + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8000) +} + +/// Default address to bind the RPC server to. +const fn default_upload_listen_address() -> SocketAddr { + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8001) +} + +/// Default number of parallel prove commits. +const fn default_parallel_prove_commits() -> NonZero { + // SAFETY: 2 != 0 + unsafe { NonZero::new_unchecked(2) } +} + +fn default_node_address() -> Url { + Url::parse(DEFAULT_NODE_ADDRESS).expect("DEFAULT_NODE_ADDRESS must be a valid Url") +} + +#[derive(Debug, Clone, Deserialize, Serialize, Args)] +#[group(multiple = true, conflicts_with = "config")] +#[serde(deny_unknown_fields)] +pub struct ConfigurationArgs { + /// The server's listen address. + #[serde(default = "default_upload_listen_address")] + #[arg(long, default_value_t = default_upload_listen_address())] + pub(crate) upload_listen_address: SocketAddr, + + /// The server's listen address. + #[serde(default = "default_rpc_listen_address")] + #[arg(long, default_value_t = default_rpc_listen_address())] + pub(crate) rpc_listen_address: SocketAddr, + + /// The target parachain node's address. + #[serde(default = "default_node_address")] + #[arg(long, default_value_t = default_node_address())] + pub(crate) node_url: Url, + + /// RocksDB storage directory. + /// Defaults to a temporary random directory, like `/tmp//deals_database`. + #[arg(long)] + pub(crate) database_directory: Option, + + /// Piece storage directory. + /// Defaults to a temporary random directory, like `/tmp//...`. + #[arg(long)] + pub(crate) storage_directory: Option, + + /// The number of prove commits to be run in parallel. + /// MUST BE > 0 or the pipeline will not progress. + /// + /// Creating a replica is memory-heavy process. + /// E.g. With 2KiB sector sizes and 16GiB of RAM, it goes OOM at 4 parallel. + #[serde(default = "default_parallel_prove_commits")] + #[arg(long, default_value_t = default_parallel_prove_commits())] + pub(crate) parallel_prove_commits: NonZero, + + // NOTE: the following parameters are marked as "not required" so the CLI doesn't require them + // when --config is used, otherwise, they're very much required + /// Proof of Replication proof type. + #[serde(default = "RegisteredSealProof::_2KiB")] + #[arg(long, required = false)] + pub(crate) seal_proof: RegisteredSealProof, + + /// Proof of Spacetime proof type. + #[serde(default = "RegisteredPoStProof::_2KiB")] + #[arg(long, required = false)] + pub(crate) post_proof: RegisteredPoStProof, + + /// Proving Parameters for PoRep proof, corresponding to given `seal_proof` sector size. + /// They are shared across all of the nodes in the network, as the chain stores corresponding Verifying Key parameters. + /// + /// Testing/temporary parameters can be generated via `polka-storage-provider-client proofs porep-params` command. + /// Note that when you generate keys, for local testnet, + /// **they need to be set** via an extrinsic pallet-proofs::set_porep_verifyingkey. + #[arg(long, required = false)] + pub(crate) porep_parameters: PathBuf, + + /// Proving Parameters for PoSt proof, corresponding to given `post_proof` sector size. + /// They are shared across all of the nodes in the network, as the chain stores corresponding Verifying Key parameters. + /// + /// Testing/temporary parameters can be generated via `polka-storage-provider-client proofs post-params` command. + /// Note that when you generate keys, for local testnet, + /// **they need to be set** via an extrinsic pallet-proofs::set_post_verifyingkey. + #[arg(long, required = false)] + pub(crate) post_parameters: PathBuf, +} diff --git a/storage-provider/server/src/main.rs b/storage-provider/server/src/main.rs index 72de35d8f..34170ff36 100644 --- a/storage-provider/server/src/main.rs +++ b/storage-provider/server/src/main.rs @@ -2,14 +2,16 @@ #![warn(unused_crate_dependencies)] #![deny(clippy::unwrap_used)] +mod config; mod db; mod pipeline; mod rpc; mod storage; -use std::{env::temp_dir, net::SocketAddr, num::NonZero, path::PathBuf, sync::Arc, time::Duration}; +use std::{env::temp_dir, net::SocketAddr, path::PathBuf, sync::Arc, time::Duration}; use clap::Parser; +use config::ConfigurationArgs; use pipeline::types::PipelineMessage; use polka_storage_proofs::{ porep::{self, PoRepParameters}, @@ -19,19 +21,14 @@ use polka_storage_provider_common::rpc::ServerInfo; use primitives::proofs::{RegisteredPoStProof, RegisteredSealProof}; use rand::Rng; use storagext::{ - multipair::{DebugPair, MultiPairSigner}, + multipair::{MultiPairArgs, MultiPairSigner}, runtime::runtime_types::{ bounded_collections::bounded_vec::BoundedVec, pallet_storage_provider::storage_provider::StorageProviderState, }, MarketClientExt, StorageProviderClientExt, }; -use subxt::{ - ext::sp_core::{ - ecdsa::Pair as ECDSAPair, ed25519::Pair as Ed25519Pair, sr25519::Pair as Sr25519Pair, - }, - tx::Signer, -}; +use subxt::{self, tx::Signer}; use tokio::{ sync::{mpsc::UnboundedReceiver, Semaphore}, task::JoinError, @@ -48,32 +45,26 @@ use crate::{ storage::{start_upload_server, StorageServerState}, }; -/// Default address to bind the RPC server to. -pub(crate) const DEFAULT_RPC_LISTEN_ADDRESS: &str = "127.0.0.1:8000"; - /// Default parachain node adress. -const DEFAULT_NODE_ADDRESS: &str = "ws://127.0.0.1:42069"; - -/// Default address to bind the RPC server to. -const DEFAULT_UPLOAD_LISTEN_ADDRESS: &str = "127.0.0.1:8001"; +pub(crate) const DEFAULT_NODE_ADDRESS: &str = "ws://127.0.0.1:42069"; /// Retry interval to connect to the parachain RPC. -const RETRY_INTERVAL: Duration = Duration::from_secs(10); +pub(crate) const RETRY_INTERVAL: Duration = Duration::from_secs(10); /// Number of retries to connect to the parachain RPC. -const RETRY_NUMBER: u32 = 5; +pub(crate) const RETRY_NUMBER: u32 = 5; /// Name for the directory where the CAR wrapped pieces are kept. -const CAR_PIECE_DIRECTORY_NAME: &str = "car"; +pub(crate) const CAR_PIECE_DIRECTORY_NAME: &str = "car"; /// Name for the directory where the unsealed pieces are kept. -const UNSEALED_SECTOR_DIRECTORY_NAME: &str = "unsealed"; +pub(crate) const UNSEALED_SECTOR_DIRECTORY_NAME: &str = "unsealed"; /// Name for the directory where the sealed pieces are kept. -const SEALED_SECTOR_DIRECTORY_NAME: &str = "sealed"; +pub(crate) const SEALED_SECTOR_DIRECTORY_NAME: &str = "sealed"; /// Name for the directory where the sealing cache is kept. -const SEALING_CACHE_DIRECTORY_NANE: &str = "cache"; +pub(crate) const SEALING_CACHE_DIRECTORY_NANE: &str = "cache"; fn get_random_temporary_folder() -> PathBuf { temp_dir().join( @@ -107,7 +98,7 @@ fn main() -> Result<(), ServerError> { .init(); // Run requested command. - let configuration: ServerConfiguration = ServerArguments::parse().try_into()?; + let configuration: Server = ServerCli::parse().try_into()?; tokio::runtime::Builder::new_multi_thread() .enable_all() @@ -165,92 +156,38 @@ pub enum ServerError { #[error(transparent)] Join(#[from] JoinError), -} - -/// The server arguments, as passed by the user, unvalidated. -#[derive(Debug, Parser)] -#[command(author, version, about, long_about = None)] -pub struct ServerArguments { - /// The server's listen address. - #[arg(long, default_value = DEFAULT_UPLOAD_LISTEN_ADDRESS)] - upload_listen_address: SocketAddr, - /// The server's listen address. - #[arg(long, default_value = DEFAULT_RPC_LISTEN_ADDRESS)] - rpc_listen_address: SocketAddr, - - /// The target parachain node's address. - #[arg(long, default_value = DEFAULT_NODE_ADDRESS)] - node_url: Url, + #[error("Invalid config: {0}")] + InvalidConfig(&'static str), - /// Sr25519 keypair, encoded as hex, BIP-39 or a dev phrase like `//Alice`. - /// - /// See `sp_core::crypto::Pair::from_string_with_seed` for more information. - #[arg(long, value_parser = DebugPair::::value_parser)] - sr25519_key: Option>, - - /// ECDSA keypair, encoded as hex, BIP-39 or a dev phrase like `//Alice`. - /// - /// See `sp_core::crypto::Pair::from_string_with_seed` for more information. - #[arg(long, value_parser = DebugPair::::value_parser)] - ecdsa_key: Option>, - - /// Ed25519 keypair, encoded as hex, BIP-39 or a dev phrase like `//Alice`. - /// - /// See `sp_core::crypto::Pair::from_string_with_seed` for more information. - #[arg(long, value_parser = DebugPair::::value_parser)] - ed25519_key: Option>, - - /// RocksDB storage directory. - /// Defaults to a temporary random directory, like `/tmp//deals_database`. - #[arg(long)] - database_directory: Option, + #[error(transparent)] + Toml(#[from] toml::de::Error), - /// Piece storage directory. - /// Defaults to a temporary random directory, like `/tmp//...`. - #[arg(long)] - storage_directory: Option, + #[error(transparent)] + Json(#[from] serde_json::Error), +} - /// Proof of Replication proof type. - #[arg(long)] - seal_proof: RegisteredSealProof, +/// The server arguments, as passed by the user, unvalidated. +#[derive(Debug, Parser)] +#[command(author, version, about, long_about = None, arg_required_else_help = true)] +pub struct ServerCli { + // Shorthand for all the keys + #[command(flatten)] + multipair: MultiPairArgs, - /// Proof of Spacetime proof type. - #[arg(long)] - post_proof: RegisteredPoStProof, + #[command(flatten)] + args: Option, - /// Proving Parameters for PoRep proof, corresponding to given `seal_proof` sector size. - /// They are shared across all of the nodes in the network, as the chain stores corresponding Verifying Key parameters. - /// - /// Testing/temporary parameters can be generated via `polka-storage-provider-client proofs porep-params` command. - /// Note that when you generate keys, for local testnet, - /// **they need to be set** via an extrinsic pallet-proofs::set_porep_verifyingkey. + /// Path to the server configuration file. #[arg(long)] - porep_parameters: PathBuf, - - /// Proving Parameters for PoSt proof, corresponding to given `post_proof` sector size. - /// They are shared across all of the nodes in the network, as the chain stores corresponding Verifying Key parameters. - /// - /// Testing/temporary parameters can be generated via `polka-storage-provider-client proofs post-params` command. - /// Note that when you generate keys, for local testnet, - /// **they need to be set** via an extrinsic pallet-proofs::set_post_verifyingkey. - #[arg(long)] - post_parameters: PathBuf, - - /// The number of prove commits to be run in parallel. - /// MUST BE > 0 or the pipeline will not progress. - /// - /// Creating a replica is memory-heavy process. - /// E.g. With 2KiB sector sizes and 16GiB of RAM, it goes OOM at 4 parallel. - #[arg(long, default_value = "2")] - parallel_prove_commits: NonZero, + config: Option, } /// A valid server configuration. To be created using [`ServerConfiguration::try_from`]. /// /// The main difference to [`Server`] is that this structure only contains validated and /// ready to use parameters. -pub struct ServerConfiguration { +pub struct Server { /// Storage server listen address. upload_listen_address: SocketAddr, @@ -288,23 +225,42 @@ pub struct ServerConfiguration { parallel_prove_commits: usize, } -impl TryFrom for ServerConfiguration { +impl TryFrom for Server { type Error = ServerError; - fn try_from(value: ServerArguments) -> Result { - if value.post_proof.sector_size() != value.seal_proof.sector_size() { + fn try_from(value: ServerCli) -> Result { + let args: ConfigurationArgs = if let Some(config) = value.config { + let config = config.canonicalize()?; + match config.extension() { + Some(ext) if ext == "toml" => { + let config = std::fs::read_to_string(config)?; + // NOTE: without the type anotation a warning about 2024 edition is issued + toml::from_str::(&config)? + } + Some(ext) if ext == "json" => { + serde_json::from_reader(std::fs::File::open(config)?)? + } + Some(ext) => { + println!("{:?}", ext); + return Err(ServerError::InvalidConfig("unsupported file format")); + } + None => return Err(ServerError::InvalidConfig("could not detect file format")), + } + } else { + value.args.expect( + "if `config == None` and `args_required_else_help = true`, then args must be Some", + ) + }; + + if args.post_proof.sector_size() != args.seal_proof.sector_size() { return Err(ServerError::SectorSizeMismatch); } - let multi_pair_signer = MultiPairSigner::new( - value.sr25519_key.map(DebugPair::::into_inner), - value.ecdsa_key.map(DebugPair::::into_inner), - value.ed25519_key.map(DebugPair::::into_inner), - ) - .ok_or(ServerError::MissingKeypair)?; + let multi_pair_signer = + Option::::from(value.multipair).ok_or(ServerError::MissingKeypair)?; let common_folder = get_random_temporary_folder(); - let database_directory = value.database_directory.unwrap_or_else(|| { + let database_directory = args.database_directory.unwrap_or_else(|| { let path = common_folder.join("deals_database"); tracing::warn!( "no database directory was defined, using: {}", @@ -314,7 +270,7 @@ impl TryFrom for ServerConfiguration { }); std::fs::create_dir_all(&database_directory)?; - let storage_directory = value.storage_directory.unwrap_or_else(|| { + let storage_directory = args.storage_directory.unwrap_or_else(|| { let path = common_folder.join("deals_storage"); tracing::warn!( "no storage directory was defined, using: {}", @@ -324,29 +280,31 @@ impl TryFrom for ServerConfiguration { }); std::fs::create_dir_all(&storage_directory)?; - let porep_parameters = porep::load_groth16_parameters(value.porep_parameters.clone()) - .map_err(|e| ServerError::InvalidPoRepParameters(value.porep_parameters, e))?; + let porep_parameters = args.porep_parameters; + let porep_parameters = porep::load_groth16_parameters(porep_parameters.clone()) + .map_err(|e| ServerError::InvalidPoRepParameters(porep_parameters, e))?; - let post_parameters = post::load_groth16_parameters(value.post_parameters.clone()) - .map_err(|e| ServerError::InvalidPoStParameters(value.post_parameters, e))?; + let post_parameters = args.post_parameters; + let post_parameters = post::load_groth16_parameters(post_parameters.clone()) + .map_err(|e| ServerError::InvalidPoStParameters(post_parameters, e))?; Ok(Self { - upload_listen_address: value.upload_listen_address, - rpc_listen_address: value.rpc_listen_address, - node_url: value.node_url, + upload_listen_address: args.upload_listen_address, + rpc_listen_address: args.rpc_listen_address, + node_url: args.node_url, multi_pair_signer, database_directory, storage_directory, - seal_proof: value.seal_proof, - post_proof: value.post_proof, + seal_proof: args.seal_proof, + post_proof: args.post_proof, porep_parameters, post_parameters, - parallel_prove_commits: value.parallel_prove_commits.get(), + parallel_prove_commits: args.parallel_prove_commits.get(), }) } } -impl ServerConfiguration { +impl Server { pub async fn run(self) -> Result<(), ServerError> { let SetupOutput { storage_state, @@ -411,7 +369,7 @@ impl ServerConfiguration { } async fn setup(self) -> Result { - let (xt_client, storage_provider_info) = ServerConfiguration::setup_storagext_client( + let (xt_client, storage_provider_info) = Server::setup_storagext_client( self.node_url, &self.multi_pair_signer, &self.post_proof, @@ -530,7 +488,7 @@ impl ServerConfiguration { None => { tracing::error!(concat!( "the provider key did not match a registered account id, ", - "you can register your account using the ", + "you can register your account using ", "`storagext-cli storage-provider register`" ));