diff --git a/doc/jcli/rest.md b/doc/jcli/rest.md index 82152f386a..c280b53b72 100644 --- a/doc/jcli/rest.md +++ b/doc/jcli/rest.md @@ -212,7 +212,7 @@ value: 990 Fetches node settings ``` -jcli rest v0 node settings get +jcli rest v0 settings get ``` The options are @@ -249,3 +249,62 @@ The options are - -h - see [conventions](#conventions) - --debug - see [conventions](#conventions) + +## Get leaders + +Fetches list of leader IDs + +``` +jcli rest v0 leaders get +``` + +The options are + +- -h - see [conventions](#conventions) +- --debug - see [conventions](#conventions) +- --output-format - see [conventions](#conventions) + + +YAML printed on success + +```yaml +--- +- 1 # list of leader IDs +- 2 +``` + +## Register leader + +Register new leader and get its ID + +``` +jcli rest v0 leaders post +``` + +The options are + +- -h - see [conventions](#conventions) +- --debug - see [conventions](#conventions) +- --output-format - see [conventions](#conventions) +-f, --file - File containing YAML with leader secret. It must have the same format as secret YAML passed to Jormungandr as --secret. If not provided, YAML will be read from stdin. + +On success created leader ID is printed + +``` +3 +``` + +## Delete leader + +Delete leader with given ID + +``` +jcli rest v0 leaders delete +``` + + - ID of deleted leader + +The options are + +- -h - see [conventions](#conventions) +- --debug - see [conventions](#conventions) diff --git a/jcli/src/jcli_app/rest/mod.rs b/jcli/src/jcli_app/rest/mod.rs index a7f9147254..6e02e33d5f 100644 --- a/jcli/src/jcli_app/rest/mod.rs +++ b/jcli/src/jcli_app/rest/mod.rs @@ -1,6 +1,6 @@ mod v0; -use jcli_app::utils::{host_addr, output_format}; +use jcli_app::utils::{host_addr, io::ReadYamlError, output_format, CustomErrorFiller}; use structopt::StructOpt; /// Send request to node REST API @@ -11,24 +11,35 @@ pub enum Rest { V0(v0::V0), } -const SERIALIZATION_ERROR_MSG: &'static str = "node returned malformed data"; +const DESERIALIZATION_ERROR_MSG: &'static str = "node returned malformed data"; custom_error! {pub Error ReqwestError { source: reqwest::Error } = @{ reqwest_error_msg(source) }, HostAddrError { source: host_addr::Error } = "invalid host address", - SerializationError { source: serde_json::Error } = @{{ let _ = source; SERIALIZATION_ERROR_MSG }}, + DeserializationError { source: serde_json::Error } = @{{ let _ = source; DESERIALIZATION_ERROR_MSG }}, OutputFormatFailed { source: output_format::Error } = "formatting output failed", InputFileInvalid { source: std::io::Error } = "could not read input file", + InputFileYamlMalformed { source: serde_yaml::Error } = "input yaml is not valid", + InputSerializationFailed { source: serde_json::Error, filler: CustomErrorFiller } = "failed to serialize input", InputHexMalformed { source: hex::Error } = "input hex encoding is not valid", } +impl From for Error { + fn from(error: ReadYamlError) -> Self { + match error { + ReadYamlError::Io { source } => Error::InputFileInvalid { source }, + ReadYamlError::Yaml { source } => Error::InputFileYamlMalformed { source }, + } + } +} + fn reqwest_error_msg(err: &reqwest::Error) -> &'static str { if err.is_timeout() { "connection with node timed out" } else if err.is_http() { "could not connect with node" } else if err.is_serialization() { - SERIALIZATION_ERROR_MSG + DESERIALIZATION_ERROR_MSG } else if err.is_redirect() { "redirecting error while connecting with node" } else if err.is_client_error() { diff --git a/jcli/src/jcli_app/rest/v0/leaders/mod.rs b/jcli/src/jcli_app/rest/v0/leaders/mod.rs new file mode 100644 index 0000000000..1aa09e55bb --- /dev/null +++ b/jcli/src/jcli_app/rest/v0/leaders/mod.rs @@ -0,0 +1,87 @@ +use jcli_app::rest::Error; +use jcli_app::utils::{io, DebugFlag, HostAddr, OutputFormat, RestApiSender}; +use std::path::PathBuf; +use structopt::StructOpt; + +#[derive(StructOpt)] +#[structopt(rename_all = "kebab-case")] +pub enum Leaders { + /// Get list of leader IDs + Get { + #[structopt(flatten)] + addr: HostAddr, + #[structopt(flatten)] + debug: DebugFlag, + #[structopt(flatten)] + output_format: OutputFormat, + }, + /// Register new leader and get its ID + Post { + #[structopt(flatten)] + addr: HostAddr, + #[structopt(flatten)] + debug: DebugFlag, + /// File containing YAML with leader secret. + /// It must have the same format as secret YAML passed to Jormungandr as --secret. + /// If not provided, YAML will be read from stdin. + #[structopt(short, long)] + file: Option, + }, + /// Delete leader + Delete { + #[structopt(flatten)] + addr: HostAddr, + #[structopt(flatten)] + debug: DebugFlag, + /// ID of deleted leader + id: u32, + }, +} + +impl Leaders { + pub fn exec(self) -> Result<(), Error> { + match self { + Leaders::Get { + addr, + debug, + output_format, + } => get(addr, debug, output_format), + Leaders::Post { addr, debug, file } => post(addr, debug, file), + Leaders::Delete { id, addr, debug } => delete(addr, debug, id), + } + } +} + +fn get(addr: HostAddr, debug: DebugFlag, output_format: OutputFormat) -> Result<(), Error> { + let url = addr.with_segments(&["v0", "leaders"])?.into_url(); + let builder = reqwest::Client::new().get(url); + let response = RestApiSender::new(builder, &debug).send()?; + response.response().error_for_status_ref()?; + let leaders = response.body().json_value()?; + let formatted = output_format.format_json(leaders)?; + println!("{}", formatted); + Ok(()) +} + +fn post(addr: HostAddr, debug: DebugFlag, file: Option) -> Result<(), Error> { + let url = addr.with_segments(&["v0", "leaders"])?.into_url(); + let builder = reqwest::Client::new().post(url); + let input: serde_json::Value = io::read_yaml(&file)?; + let response = RestApiSender::new(builder, &debug) + .with_json_body(&input)? + .send()?; + response.response().error_for_status_ref()?; + println!("{}", response.body().text().as_ref()); + Ok(()) +} + +fn delete(addr: HostAddr, debug: DebugFlag, id: u32) -> Result<(), Error> { + let url = addr + .with_segments(&["v0", "leaders", &id.to_string()])? + .into_url(); + let builder = reqwest::Client::new().delete(url); + let response = RestApiSender::new(builder, &debug).send()?; + response.response().error_for_status_ref()?; + println!("Success"); + Ok(()) +} diff --git a/jcli/src/jcli_app/rest/v0/mod.rs b/jcli/src/jcli_app/rest/v0/mod.rs index 90c91f2e4d..aafeffe5a5 100644 --- a/jcli/src/jcli_app/rest/v0/mod.rs +++ b/jcli/src/jcli_app/rest/v0/mod.rs @@ -1,5 +1,6 @@ mod account; mod block; +mod leaders; mod message; mod node; mod settings; @@ -17,6 +18,8 @@ pub enum V0 { Account(account::Account), /// Block operations Block(block::Block), + /// Node leaders operations + Leaders(leaders::Leaders), /// Message sending Message(message::Message), /// Node information @@ -36,6 +39,7 @@ impl V0 { match self { V0::Account(account) => account.exec(), V0::Block(block) => block.exec(), + V0::Leaders(leaders) => leaders.exec(), V0::Message(message) => message.exec(), V0::Node(node) => node.exec(), V0::Settings(settings) => settings.exec(), diff --git a/jcli/src/jcli_app/utils/io.rs b/jcli/src/jcli_app/utils/io.rs index 24ad7990c9..7270cc41dc 100644 --- a/jcli/src/jcli_app/utils/io.rs +++ b/jcli/src/jcli_app/utils/io.rs @@ -1,3 +1,4 @@ +use serde::de::DeserializeOwned; use std::io::{stdin, stdout, BufRead, BufReader, Error, Write}; use std::path::Path; use std::path::PathBuf; @@ -48,3 +49,14 @@ pub fn read_line>(path: &Option

) -> Result { open_file_read(path)?.read_line(&mut line)?; Ok(line.trim_end().to_string()) } + +custom_error! { pub ReadYamlError + Io { source: Error } = "could not read input", + Yaml { source: serde_yaml::Error } = "input contains malformed yaml", +} + +pub fn read_yaml(path: &Option>) -> Result { + let reader = open_file_read(path)?; + let yaml = serde_yaml::from_reader(reader)?; + Ok(yaml) +} diff --git a/jcli/src/jcli_app/utils/mod.rs b/jcli/src/jcli_app/utils/mod.rs index f14406e966..d70603b60d 100644 --- a/jcli/src/jcli_app/utils/mod.rs +++ b/jcli/src/jcli_app/utils/mod.rs @@ -10,6 +10,7 @@ pub mod output_format; pub use self::account_id::AccountId; pub use self::debug_flag::DebugFlag; +pub use self::error::CustomErrorFiller; pub use self::host_addr::HostAddr; pub use self::output_format::OutputFormat; pub use self::rest_api::{RestApiResponse, RestApiResponseBody, RestApiSender}; diff --git a/jcli/src/jcli_app/utils/rest_api.rs b/jcli/src/jcli_app/utils/rest_api.rs index f95f69debe..9d512f0c94 100644 --- a/jcli/src/jcli_app/utils/rest_api.rs +++ b/jcli/src/jcli_app/utils/rest_api.rs @@ -1,6 +1,7 @@ use hex; use jcli_app::utils::DebugFlag; use reqwest::{Error, RequestBuilder, Response}; +use serde::Serialize; use std::{fmt, io::Write}; pub struct RestApiSender<'a> { @@ -42,6 +43,21 @@ impl<'a> RestApiSender<'a> { self } + pub fn with_json_body(mut self, body: &impl Serialize) -> Result { + let json = serde_json::to_string(body)?; + if self.debug_flag.debug_writer().is_some() { + self.request_body_debug = Some(json.clone()); + } + self.builder = self + .builder + .header( + reqwest::header::CONTENT_TYPE, + mime::APPLICATION_JSON.as_ref(), + ) + .body(json.into_bytes()); + Ok(self) + } + pub fn send(self) -> Result { let request = self.builder.build()?; if let Some(mut writer) = self.debug_flag.debug_writer() { diff --git a/jormungandr/src/leadership/task.rs b/jormungandr/src/leadership/task.rs index 97523d70b9..97bd673610 100644 --- a/jormungandr/src/leadership/task.rs +++ b/jormungandr/src/leadership/task.rs @@ -139,11 +139,12 @@ fn handle_leadership( blockchain_tip.hash().unwrap(), ); - let block = enclave.create_block(block, scheduled_event.leader_output); + if let Some(block) = enclave.create_block(block, scheduled_event.leader_output) { + block_message + .try_send(BlockMsg::LeadershipBlock(block)) + .unwrap() + } - block_message - .try_send(BlockMsg::LeadershipBlock(block)) - .unwrap(); stats_counter.set_slot_start_time(scheduled_event.expected_time.into()); future::ok(()) }) diff --git a/jormungandr/src/main.rs b/jormungandr/src/main.rs index 53c11b7d24..012f056836 100644 --- a/jormungandr/src/main.rs +++ b/jormungandr/src/main.rs @@ -229,6 +229,7 @@ fn start_services(bootstrapped_node: BootstrappedNode) -> Result<(), start_up::E transaction_task: Arc::new(Mutex::new(fragment_msgbox)), logs: Arc::new(Mutex::new(pool_logs)), server: Arc::default(), + enclave, }; Some(rest::start_rest_server(&rest, context)?) } diff --git a/jormungandr/src/rest/mod.rs b/jormungandr/src/rest/mod.rs index 2e40018754..5c056495b7 100644 --- a/jormungandr/src/rest/mod.rs +++ b/jormungandr/src/rest/mod.rs @@ -10,6 +10,7 @@ use std::sync::{Arc, Mutex, RwLock}; use crate::blockchain::BlockchainR; use crate::fragment::Logs; +use crate::secure::enclave::Enclave; use crate::settings::start::{Error as ConfigError, Rest}; use crate::stats_counter::StatsCounter; @@ -23,6 +24,7 @@ pub struct Context { pub transaction_task: Arc>>, pub logs: Arc>, pub server: Arc>>, + pub enclave: Enclave, } pub fn start_rest_server(config: &Rest, context: Context) -> Result { diff --git a/jormungandr/src/rest/v0/handlers.rs b/jormungandr/src/rest/v0/handlers.rs index e3235d9a81..b320c3a473 100644 --- a/jormungandr/src/rest/v0/handlers.rs +++ b/jormungandr/src/rest/v0/handlers.rs @@ -9,16 +9,17 @@ use chain_crypto::{Blake2b256, PublicKey}; use chain_impl_mockchain::account::{AccountAlg, Identifier}; use chain_impl_mockchain::fragment::Fragment; use chain_impl_mockchain::key::Hash; -use chain_impl_mockchain::leadership::LeadershipConsensus; -use chain_impl_mockchain::ledger::Ledger; +use chain_impl_mockchain::leadership::{Leader, LeadershipConsensus}; use chain_impl_mockchain::value::{Value, ValueError}; use chain_storage::store; +use crate::intercom::TransactionMsg; +use crate::secure::enclave::LeaderId; +use crate::secure::NodeSecret; use bytes::{Bytes, IntoBuf}; use futures::Future; use std::str::FromStr; -use crate::intercom::TransactionMsg; pub use crate::rest::Context; pub fn get_utxos(context: State) -> impl Responder { @@ -255,3 +256,28 @@ pub fn get_shutdown(context: State) -> Result { .stop(); Ok(HttpResponse::Ok().finish()) } + +pub fn get_leaders(context: State) -> impl Responder { + Json(json! { + context.enclave.get_leaderids() + }) +} + +pub fn post_leaders(secret: Json, context: State) -> impl Responder { + let leader = Leader { + bft_leader: secret.bft(), + genesis_leader: secret.genesis(), + }; + let leader_id = context.enclave.add_leader(leader); + Json(leader_id) +} + +pub fn delete_leaders( + context: State, + leader_id: Path, +) -> Result { + match context.enclave.remove_leader(*leader_id) { + true => Ok(HttpResponse::Ok().finish()), + false => Err(ErrorNotFound("Leader with given ID not found")), + } +} diff --git a/jormungandr/src/rest/v0/mod.rs b/jormungandr/src/rest/v0/mod.rs index 4fdf649077..7c94113d38 100644 --- a/jormungandr/src/rest/v0/mod.rs +++ b/jormungandr/src/rest/v0/mod.rs @@ -17,6 +17,13 @@ pub fn app(context: handlers::Context) -> App { .resource("/fragment/logs", |r| { r.get().with(handlers::get_message_logs) }) + .resource("/leaders", |r| { + r.get().with(handlers::get_leaders); + r.post().with(handlers::post_leaders); + }) + .resource("/leaders/{leader_id}", |r| { + r.delete().with(handlers::delete_leaders) + }) .resource("/settings", |r| r.get().with(handlers::get_settings)) .resource("/stake", |r| r.get().with(handlers::get_stake_distribution)) .resource("/shutdown", |r| r.get().with(handlers::get_shutdown)) diff --git a/jormungandr/src/secure/enclave.rs b/jormungandr/src/secure/enclave.rs index e9ddff5984..5a2cfd2c2a 100644 --- a/jormungandr/src/secure/enclave.rs +++ b/jormungandr/src/secure/enclave.rs @@ -4,7 +4,8 @@ use chain_impl_mockchain::leadership::{Leader, LeaderOutput, Leadership}; use std::collections::BTreeMap; use std::sync::{Arc, RwLock}; -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)] +#[serde(transparent)] pub struct LeaderId(u32); impl LeaderId { @@ -48,7 +49,7 @@ impl Enclave { leaders.keys().map(|v| v.clone()).collect() } - pub fn add_leader(&mut self, leader: Leader) -> LeaderId { + pub fn add_leader(&self, leader: Leader) -> LeaderId { let mut leaders = self.leaders.write().unwrap(); let next_leader_id = get_maximum_id(&leaders).next(); // This panic case should never happens in practice, as this structure is @@ -60,9 +61,9 @@ impl Enclave { next_leader_id } - pub fn remove_leader(&mut self, leader_id: LeaderId) { + pub fn remove_leader(&self, leader_id: LeaderId) -> bool { let mut leaders = self.leaders.write().unwrap(); - leaders.remove(&leader_id); + leaders.remove(&leader_id).is_some() } // temporary method @@ -125,13 +126,10 @@ impl Enclave { output } - pub fn create_block(&self, block: BlockBuilder, event: LeaderEvent) -> Block { + pub fn create_block(&self, block: BlockBuilder, event: LeaderEvent) -> Option { let leaders = self.leaders.read().unwrap(); - let leader = match leaders.get(&event.id) { - None => panic!("leader is gone while creating a block"), - Some(l) => l, - }; - match event.output { + let leader = leaders.get(&event.id)?; + let block = match event.output { LeaderOutput::None => unreachable!("Output::None are supposed to be filtered out"), LeaderOutput::Bft(_) => { if let Some(ref leader) = &leader.bft_leader { @@ -151,6 +149,7 @@ impl Enclave { unreachable!("the leader was elected for Genesis Praos signing block, we expect it has the signing key") } } - } + }; + Some(block) } }