diff --git a/sui/Cargo.toml b/sui/Cargo.toml index 80ee8cc89134a..22305efa354e0 100644 --- a/sui/Cargo.toml +++ b/sui/Cargo.toml @@ -41,6 +41,12 @@ rustyline-derive = "0.6.0" colored = "2.0.0" unescape = "0.1.0" +# Deps for rest server +dropshot = "0.6.0" +http = "0.2.6" +hyper = "0.14.16" +schemars = "0.8.8" + move-package = { git = "https://github.com/diem/move", rev = "7683d09732dd930c581583bf5fde97fb7ac02ff7" } move-core-types = { git = "https://github.com/diem/move", rev = "7683d09732dd930c581583bf5fde97fb7ac02ff7", features = ["address20"] } move-bytecode-verifier = { git = "https://github.com/diem/move", rev = "7683d09732dd930c581583bf5fde97fb7ac02ff7" } @@ -65,3 +71,7 @@ path = "src/sui.rs" [[bin]] name = "sui-move" path = "src/sui-move.rs" + +[[bin]] +name = "rest_server" +path = "src/rest_server.rs" \ No newline at end of file diff --git a/sui/src/rest_server.rs b/sui/src/rest_server.rs new file mode 100644 index 0000000000000..fec79c0e6fa1a --- /dev/null +++ b/sui/src/rest_server.rs @@ -0,0 +1,339 @@ +// Copyright (c) 2022, Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use dropshot::endpoint; +use dropshot::{ + ApiDescription, ConfigDropshot, ConfigLogging, ConfigLoggingLevel, HttpError, HttpResponseOk, + HttpResponseUpdatedNoContent, HttpServerStarter, RequestContext, +}; +use hyper::StatusCode; +use serde_json::json; +use sui::config::{Config, GenesisConfig, NetworkConfig, WalletConfig}; +use sui::sui_commands; +use sui::wallet_commands::WalletContext; +use sui_core::authority_client::AuthorityClient; +use sui_core::client::{Client, ClientState}; +use sui_types::base_types::*; +use sui_types::committee::Committee; + +use futures::stream::{futures_unordered::FuturesUnordered, StreamExt as _}; + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::net::{Ipv6Addr, SocketAddr}; +use std::path::PathBuf; +use tokio::task::{self, JoinHandle}; +use tracing::{error, info}; + +use std::sync::{Arc, Mutex}; + +#[tokio::main] +async fn main() -> Result<(), String> { + let config_dropshot: ConfigDropshot = ConfigDropshot { + bind_address: SocketAddr::from((Ipv6Addr::LOCALHOST, 5000)), + ..Default::default() + }; + + let config_logging = ConfigLogging::StderrTerminal { + level: ConfigLoggingLevel::Info, + }; + let log = config_logging + .to_logger("rest_server") + .map_err(|error| format!("failed to create logger: {}", error))?; + + tracing_subscriber::fmt().init(); + + let mut api = ApiDescription::new(); + api.register(start).unwrap(); + api.register(genesis).unwrap(); + api.register(stop).unwrap(); + + let api_context = ServerContext::new(); + + let server = HttpServerStarter::new(&config_dropshot, api, api_context, &log) + .map_err(|error| format!("failed to create server: {}", error))? + .start(); + + server.await +} + +/** + * Server context (state shared by handler functions) + */ +struct ServerContext { + genesis_config_path: String, + wallet_config_path: String, + network_config_path: String, + authority_db_path: String, + client_db_path: Arc>, + authority_handles: Arc>>>, + wallet_context: Arc>>, +} + +impl ServerContext { + pub fn new() -> ServerContext { + ServerContext { + genesis_config_path: String::from("genesis.conf"), + wallet_config_path: String::from("wallet.conf"), + network_config_path: String::from("./network.conf"), + authority_db_path: String::from("./authorities_db"), + client_db_path: Arc::new(Mutex::new(String::new())), + authority_handles: Arc::new(Mutex::new(Vec::new())), + wallet_context: Arc::new(Mutex::new(None)), + } + } +} + +/** + * 'GenesisResponse' returns the genesis of wallet & network config. + */ +#[derive(Deserialize, Serialize, JsonSchema)] +struct GenesisResponse { + wallet_config: serde_json::Value, + network_config: serde_json::Value, +} + +/** + * [SUI] Use to provide server configurations for genesis. + */ +#[endpoint { + method = POST, + path = "/debug/sui/genesis", +}] +async fn genesis( + rqctx: Arc>, +) -> Result, HttpError> { + let server_context = rqctx.context(); + let genesis_config_path = &server_context.genesis_config_path; + let network_config_path = &server_context.network_config_path; + let wallet_config_path = &server_context.wallet_config_path; + + let mut network_config = NetworkConfig::read_or_create(&PathBuf::from(network_config_path)) + .map_err(|error| { + custom_http_error( + StatusCode::CONFLICT, + format!("Unable to read network config: {error}"), + ) + })?; + + if !network_config.authorities.is_empty() { + return Err(custom_http_error( + StatusCode::CONFLICT, + String::from("Cannot run genesis on a existing network, stop network to try again."), + )); + } + + let working_dir = network_config.config_path().parent().unwrap().to_owned(); + let genesis_conf = GenesisConfig::default_genesis(&working_dir.join(genesis_config_path)) + .map_err(|error| { + custom_http_error( + StatusCode::CONFLICT, + format!("Unable to create default genesis configuration: {error}"), + ) + })?; + + let wallet_path = working_dir.join(wallet_config_path); + let mut wallet_config = + WalletConfig::create(&working_dir.join(wallet_path)).map_err(|error| { + custom_http_error( + StatusCode::CONFLICT, + format!("Wallet config was unable to be created: {error}"), + ) + })?; + // Need to use a random id because rocksdb locks on current process which means even if the directory is deleted + // the lock will remain causing an IO Error when a restart is attempted. + let client_db_path = format!("client_db_{:?}", ObjectID::random()); + wallet_config.db_folder_path = working_dir.join(&client_db_path); + *server_context.client_db_path.lock().unwrap() = client_db_path; + + sui_commands::genesis(&mut network_config, genesis_conf, &mut wallet_config) + .await + .map_err(|err| { + custom_http_error( + StatusCode::FAILED_DEPENDENCY, + format!("Genesis error: {:?}", err), + ) + })?; + + Ok(HttpResponseOk(GenesisResponse { + wallet_config: json!(wallet_config), + network_config: json!(network_config), + })) +} + +/** + * [SUI] Start servers with specified configurations. + */ +#[endpoint { + method = POST, + path = "/debug/sui/start", +}] +async fn start( + rqctx: Arc>, +) -> Result, HttpError> { + let server_context = rqctx.context(); + let network_config_path = &server_context.network_config_path; + + let network_config = NetworkConfig::read_or_create(&PathBuf::from(network_config_path)) + .map_err(|error| { + custom_http_error( + StatusCode::CONFLICT, + format!("Unable to read network config: {error}"), + ) + })?; + + if network_config.authorities.is_empty() { + return Err(custom_http_error( + StatusCode::CONFLICT, + String::from("No authority configured for the network, please run genesis."), + )); + } + + { + if !(*server_context.authority_handles.lock().unwrap()).is_empty() { + return Err(custom_http_error( + StatusCode::FORBIDDEN, + String::from("Sui network is already running."), + )); + } + } + + let committee = Committee::new( + network_config + .authorities + .iter() + .map(|info| (*info.key_pair.public_key_bytes(), info.stake)) + .collect(), + ); + let mut handles = FuturesUnordered::new(); + + for authority in &network_config.authorities { + let server = sui_commands::make_server( + authority, + &committee, + vec![], + &[], + network_config.buffer_size, + ) + .await + .map_err(|error| { + custom_http_error( + StatusCode::CONFLICT, + format!("Unable to make server: {error}"), + ) + })?; + handles.push(async move { + match server.spawn().await { + Ok(server) => Ok(server), + Err(err) => { + return Err(custom_http_error( + StatusCode::FAILED_DEPENDENCY, + format!("Failed to start server: {}", err), + )); + } + } + }) + } + + let num_authorities = handles.len(); + info!("Started {} authorities", num_authorities); + + while let Some(spawned_server) = handles.next().await { + server_context + .authority_handles + .lock() + .unwrap() + .push(task::spawn(async { + if let Err(err) = spawned_server.unwrap().join().await { + error!("Server ended with an error: {}", err); + } + })); + } + + let wallet_config_path = &server_context.wallet_config_path; + + let wallet_config = + WalletConfig::read_or_create(&PathBuf::from(wallet_config_path)).map_err(|error| { + custom_http_error( + StatusCode::CONFLICT, + format!("Unable to read wallet config: {error}"), + ) + })?; + + let addresses = wallet_config + .accounts + .iter() + .map(|info| info.address) + .collect::>(); + let mut wallet_context = WalletContext::new(wallet_config).map_err(|error| { + custom_http_error( + StatusCode::CONFLICT, + format!("Can't create new wallet context: {error}"), + ) + })?; + + // Sync all accounts. + for address in addresses.iter() { + let client_state = wallet_context + .get_or_create_client_state(address) + .map_err(|error| { + custom_http_error( + StatusCode::CONFLICT, + format!("Can't create client state: {error}"), + ) + })?; + if let Err(err) = sync_client_state(client_state).await { + return Err(err); + } + } + + *server_context.wallet_context.lock().unwrap() = Some(wallet_context); + + Ok(HttpResponseOk(format!( + "Started {} authorities", + num_authorities + ))) +} + +/** + * [SUI] Stop servers and delete storage. + */ +#[endpoint { + method = POST, + path = "/debug/sui/stop", +}] +async fn stop( + rqctx: Arc>, +) -> Result { + let server_context = rqctx.context(); + + for authority_handle in &*server_context.authority_handles.lock().unwrap() { + authority_handle.abort(); + } + (*server_context.authority_handles.lock().unwrap()).clear(); + + fs::remove_dir_all(server_context.client_db_path.lock().unwrap().clone()).ok(); + fs::remove_dir_all(&server_context.authority_db_path).ok(); + fs::remove_file(&server_context.network_config_path).ok(); + fs::remove_file(&server_context.wallet_config_path).ok(); + + Ok(HttpResponseUpdatedNoContent()) +} + +async fn sync_client_state( + client_state: &mut ClientState, +) -> Result<(), HttpError> { + // synchronize with authorities + let res = async move { client_state.sync_client_state().await }; + res.await.map_err(|err| { + custom_http_error( + StatusCode::FAILED_DEPENDENCY, + format!("Sync error: {:?}", err), + ) + }) +} + +fn custom_http_error(status_code: http::StatusCode, message: String) -> HttpError { + HttpError::for_client_error(None, status_code, message) +} diff --git a/sui/src/sui_commands.rs b/sui/src/sui_commands.rs index ac0ec3b2751fc..bd0d0435d3fb8 100644 --- a/sui/src/sui_commands.rs +++ b/sui/src/sui_commands.rs @@ -40,14 +40,17 @@ impl SuiCommand { match self { SuiCommand::Start => start_network(config).await, SuiCommand::Genesis { config: path } => { + // Network config has been created by this point, safe to unwrap. + let working_dir = config.config_path().parent().unwrap(); let genesis_conf = if let Some(path) = path { GenesisConfig::read(path)? } else { - // Network config has been created by this point, safe to unwrap. - let working_dir = config.config_path().parent().unwrap(); GenesisConfig::default_genesis(&working_dir.join("genesis.conf"))? }; - genesis(config, genesis_conf).await + let wallet_path = working_dir.join("wallet.conf"); + let mut wallet_config = WalletConfig::create(&wallet_path)?; + wallet_config.db_folder_path = working_dir.join("client_db"); + genesis(config, genesis_conf, &mut wallet_config).await } } } @@ -94,12 +97,11 @@ async fn start_network(config: &NetworkConfig) -> Result<(), anyhow::Error> { Ok(()) } -async fn genesis( +pub async fn genesis( config: &mut NetworkConfig, genesis_conf: GenesisConfig, + wallet_config: &mut WalletConfig, ) -> Result<(), anyhow::Error> { - // We have created the config file, safe to unwrap the path here. - let working_dir = &config.config_path().parent().unwrap().to_path_buf(); if !config.authorities.is_empty() { return Err(anyhow!("Cannot run genesis on a existing network, please delete network config file and try again.")); } @@ -219,12 +221,8 @@ async fn genesis( ) .await?; } - - let wallet_path = working_dir.join("wallet.conf"); - let mut wallet_config = WalletConfig::create(&wallet_path)?; wallet_config.authorities = authority_info; wallet_config.accounts = new_addresses; - wallet_config.db_folder_path = working_dir.join("client_db"); info!("Network genesis completed."); config.save()?; @@ -240,7 +238,7 @@ async fn genesis( Ok(()) } -async fn make_server( +pub async fn make_server( authority: &AuthorityPrivateInfo, committee: &Committee, preload_modules: Vec, diff --git a/sui/src/wallet_commands.rs b/sui/src/wallet_commands.rs index b037ba4fafafa..2a8ed9773f132 100644 --- a/sui/src/wallet_commands.rs +++ b/sui/src/wallet_commands.rs @@ -283,7 +283,7 @@ impl WalletContext { Ok(context) } - fn get_or_create_client_state( + pub fn get_or_create_client_state( &mut self, owner: &SuiAddress, ) -> Result<&mut ClientState, SuiError> {