Skip to content

Commit

Permalink
[fix] hyperledger-iroha#3473: Make kagami validator runnable from o…
Browse files Browse the repository at this point in the history
…utside `iroha` dir

Signed-off-by: Ilia Churin <churin.ilya@gmail.com>
  • Loading branch information
ilchu committed Jul 7, 2023
1 parent 19984d9 commit cc9f272
Show file tree
Hide file tree
Showing 14 changed files with 745 additions and 682 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/iroha2-dev-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ jobs:
if: always()
working-directory: wasm
run: mold --run cargo build --target wasm32-unknown-unknown --quiet
- name: Check validator generation
if: always()
run: ./scripts/check.sh validator

with_coverage:
runs-on: [self-hosted, Linux, iroha2ci]
Expand Down
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions scripts/check.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#!/bin/sh
set -e

KAGAMI_BIN_PATH=${2:-"./target/release/kagami"}

case $1 in
"docs")
cargo run --release --bin kagami -- docs | diff - docs/source/references/config.md || {
Expand All @@ -27,4 +29,14 @@ case $1 in
echo 'Please re-generate schema with `cargo run --release --bin kagami -- schema > docs/source/references/schema.json`'
exit 1
};;
"validator")
if [ ! -e "$KAGAMI_BIN_PATH" ]; then
echo 'Please run this check from Iroha root dir or provide a valid path to `kagami` binary as a second argument'
exit 1
fi
$KAGAMI_BIN_PATH validator || {
echo 'Failed to run `kagami validator` as a standalone binary without invoking `cargo`'
exit 1
}
;;
esac
1 change: 1 addition & 0 deletions tools/kagami/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ owo-colors = { version = "3.5.0", features = ["supports-colors"] }
supports-color = "2.0.0"
inquire = "0.6.2"
duct = "0.13.6"
tempfile = "3.6.0"

[build-dependencies]
eyre = "0.6.8"
Expand Down
82 changes: 82 additions & 0 deletions tools/kagami/src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
use std::str::FromStr as _;

use clap::{Parser, Subcommand};
use iroha_crypto::{Algorithm, PrivateKey, PublicKey};
use iroha_primitives::small::SmallStr;

use super::*;

#[derive(Parser, Debug, Clone, Copy)]
pub struct Args {
#[clap(subcommand)]
mode: Mode,
}

#[derive(Subcommand, Debug, Clone, Copy)]
pub enum Mode {
Client(client::Args),
Peer(peer::Args),
}

impl<T: Write> RunArgs<T> for Args {
fn run(self, writer: &mut BufWriter<T>) -> Outcome {
match self.mode {
Mode::Client(args) => args.run(writer),
Mode::Peer(args) => args.run(writer),
}
}
}

mod client {
use iroha_config::{
client::{BasicAuth, ConfigurationProxy, WebLogin},
torii::{uri::DEFAULT_API_ADDR, DEFAULT_TORII_TELEMETRY_ADDR},
};

use super::*;

#[derive(ClapArgs, Debug, Clone, Copy)]
pub struct Args;

impl<T: Write> RunArgs<T> for Args {
fn run(self, writer: &mut BufWriter<T>) -> Outcome {
let config = ConfigurationProxy {
torii_api_url: Some(format!("http://{DEFAULT_API_ADDR}").parse()?),
torii_telemetry_url: Some(format!("http://{DEFAULT_TORII_TELEMETRY_ADDR}").parse()?),
account_id: Some("alice@wonderland".parse()?),
basic_auth: Some(Some(BasicAuth {
web_login: WebLogin::new("mad_hatter")?,
password: SmallStr::from_str("ilovetea"),
})),
public_key: Some(PublicKey::from_str(
"ed01207233BFC89DCBD68C19FDE6CE6158225298EC1131B6A130D1AEB454C1AB5183C0",
)?),
private_key: Some(PrivateKey::from_hex(
Algorithm::Ed25519,
"9AC47ABF59B356E0BD7DCBBBB4DEC080E302156A48CA907E47CB6AEA1D32719E7233BFC89DCBD68C19FDE6CE6158225298EC1131B6A130D1AEB454C1AB5183C0".as_ref()
)?),
..ConfigurationProxy::default()
}
.build()?;
writeln!(writer, "{}", serde_json::to_string_pretty(&config)?)
.wrap_err("Failed to write serialized client configuration to the buffer.")
}
}
}

mod peer {
use iroha_config::iroha::ConfigurationProxy as IrohaConfigurationProxy;

use super::*;

#[derive(ClapArgs, Debug, Clone, Copy)]
pub struct Args;

impl<T: Write> RunArgs<T> for Args {
fn run(self, writer: &mut BufWriter<T>) -> Outcome {
let config = IrohaConfigurationProxy::default();
writeln!(writer, "{}", serde_json::to_string_pretty(&config)?)
.wrap_err("Failed to write serialized peer configuration to the buffer.")
}
}
}
113 changes: 113 additions & 0 deletions tools/kagami/src/crypto.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
use clap::{builder::PossibleValue, ArgGroup, ValueEnum};
use color_eyre::eyre::WrapErr as _;
use iroha_crypto::{Algorithm, KeyGenConfiguration, KeyPair, PrivateKey};

use super::*;

/// Use `Kagami` to generate cryptographic key-pairs.
#[derive(ClapArgs, Debug, Clone)]
#[command(group = ArgGroup::new("generate_from").required(false))]
#[command(group = ArgGroup::new("format").required(false))]
pub struct Args {
/// The algorithm to use for the key-pair generation
#[clap(default_value_t, long, short)]
algorithm: AlgorithmArg,
/// The `private_key` to generate the key-pair from
#[clap(long, short, group = "generate_from")]
private_key: Option<String>,
/// The `seed` to generate the key-pair from
#[clap(long, short, group = "generate_from")]
seed: Option<String>,
/// Output the key-pair in JSON format
#[clap(long, short, group = "format")]
json: bool,
/// Output the key-pair without additional text
#[clap(long, short, group = "format")]
compact: bool,
}

#[derive(Clone, Debug, Default, derive_more::Display)]
struct AlgorithmArg(Algorithm);

impl ValueEnum for AlgorithmArg {
fn value_variants<'a>() -> &'a [Self] {
// TODO: add compile-time check to ensure all variants are enumerated
&[
Self(Algorithm::Ed25519),
Self(Algorithm::Secp256k1),
Self(Algorithm::BlsNormal),
Self(Algorithm::BlsSmall),
]
}

fn to_possible_value(&self) -> Option<PossibleValue> {
Some(self.0.as_static_str().into())
}
}

impl<T: Write> RunArgs<T> for Args {
fn run(self, writer: &mut BufWriter<T>) -> Outcome {
if self.json {
let key_pair = self.key_pair()?;
let output =
serde_json::to_string_pretty(&key_pair).wrap_err("Failed to serialise to JSON.")?;
writeln!(writer, "{output}")?;
} else if self.compact {
let key_pair = self.key_pair()?;
writeln!(writer, "{}", &key_pair.public_key())?;
writeln!(writer, "{}", &key_pair.private_key())?;
writeln!(writer, "{}", &key_pair.public_key().digest_function())?;
} else {
let key_pair = self.key_pair()?;
writeln!(
writer,
"Public key (multihash): \"{}\"",
&key_pair.public_key()
)?;
writeln!(
writer,
"Private key ({}): \"{}\"",
&key_pair.public_key().digest_function(),
&key_pair.private_key()
)?;
}
Ok(())
}
}

impl Args {
fn key_pair(self) -> color_eyre::Result<KeyPair> {
let algorithm = self.algorithm.0;
let config = KeyGenConfiguration::default().with_algorithm(algorithm);

let key_pair = match (self.seed, self.private_key) {
(None, None) => KeyPair::generate_with_configuration(config),
(None, Some(private_key_hex)) => {
let private_key = PrivateKey::from_hex(algorithm, private_key_hex.as_ref())
.wrap_err("Failed to decode private key")?;
KeyPair::generate_with_configuration(config.use_private_key(private_key))
}
(Some(seed), None) => {
let seed: Vec<u8> = seed.as_bytes().into();
KeyPair::generate_with_configuration(config.use_seed(seed))
}
_ => unreachable!("Clap group invariant"),
}
.wrap_err("Failed to generate key pair")?;

Ok(key_pair)
}
}

#[cfg(test)]
mod tests {
use super::{Algorithm, AlgorithmArg};

#[test]
fn algorithm_arg_displays_as_algorithm() {
assert_eq!(
format!("{}", AlgorithmArg(Algorithm::Ed25519)),
format!("{}", Algorithm::Ed25519)
)
}
}
136 changes: 136 additions & 0 deletions tools/kagami/src/docs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
#![allow(clippy::panic_in_result_fn, clippy::expect_used)]
#![allow(
clippy::arithmetic_side_effects,
clippy::std_instead_of_core,
clippy::std_instead_of_alloc
)]
use std::{fmt::Debug, io::Write};

use color_eyre::eyre::WrapErr as _;
use iroha_config::{base::proxy::Documented, iroha::ConfigurationProxy};
use serde_json::Value;

use super::*;

impl<E: Debug, C: Documented<Error = E> + Send + Sync + Default> PrintDocs for C {}

#[derive(ClapArgs, Debug, Clone, Copy)]
pub struct Args;

impl<T: Write> RunArgs<T> for Args {
fn run(self, writer: &mut BufWriter<T>) -> crate::Outcome {
ConfigurationProxy::get_markdown(writer).wrap_err("Failed to generate documentation")
}
}

pub trait PrintDocs: Documented + Send + Sync + Default
where
Self::Error: Debug,
{
fn get_markdown<W: Write>(writer: &mut W) -> color_eyre::Result<()> {
let Value::Object(docs) = Self::get_docs() else {
unreachable!("As top level structure is always object")
};
let mut vec = Vec::new();
let defaults = serde_json::to_string_pretty(&Self::default())?;

writeln!(writer, "# Iroha Configuration reference\n")?;
writeln!(writer, "In this document we provide a reference and detailed descriptions of Iroha's configuration options. \
The options have different underlying types and default values, which are denoted in code as types wrapped in a single \
`Option<..>` or in a double `Option<Option<..>>`. For the detailed explanation, please refer to \
this [section](#configuration-types).\n")?;
writeln!(
writer,
"## Configuration types\n\n\
### `Option<..>`\n\n\
A type wrapped in a single `Option<..>` signifies that in the corresponding `json` block there is a fallback value for this type, \
and that it only serves as a reference. If a default for such a type has a `null` value, it means that there is no meaningful fallback \
available for this particular value.\n\nAll the default values can be freely obtained from a provided [sample configuration file](../../../configs/peer/config.json), \
but it should only serve as a starting point. If left unchanged, the sample configuration file would still fail to build due to it having `null` in place of \
[public](#public_key) and [private](#private_key) keys as well as [endpoint](#torii.api_url) [URLs](#torii.telemetry_url). \
These should be provided either by modifying the sample config file or as environment variables. \
No other overloading of configuration values happens besides reading them from a file and capturing the environment variables.\n\n\
For both types of configuration options wrapped in a single `Option<..>` (i.e. both those that have meaningful defaults and those that have `null`), \
failure to provide them in any of the above two ways results in an error.\n\n\
### `Option<Option<..>>`\n\n\
`Option<Option<..>>` types should be distinguished from types wrapped in a single `Option<..>`. Only the double option ones are allowed to stay `null`, \
meaning that **not** providing them in an environment variable or a file will **not** result in an error.\n\n\
Thus, only these types are truly optional in the mundane sense of the word. \
An example of this distinction is genesis [public](#genesis.account_public_key) and [private](#genesis.account_private_key) key. \
While the first one is a single `Option<..>` wrapped type, the latter is wrapped in `Option<Option<..>>`. This means that the genesis *public* key should always be \
provided by the user, be it via a file config or an environment variable, whereas the *private* key is only needed for the peer that submits the genesis block, \
and can be omitted for all others. The same logic goes for other double option fields such as logger file path.\n\n\
### Sumeragi: default `null` values\n\n\
A special note about sumeragi fields with `null` as default: only the [`trusted_peers`](#sumeragi.trusted_peers) field out of the three can be initialized via a \
provided file or an environment variable.\n\n\
The other two fields, namely [`key_pair`](#sumeragi.key_pair) and [`peer_id`](#sumeragi.peer_id), go through a process of finalization where their values \
are derived from the corresponding ones in the uppermost Iroha config (using its [`public_key`](#public_key) and [`private_key`](#private_key) fields) \
or the Torii config (via its [`p2p_addr`](#torii.p2p_addr)). \
This ensures that these linked fields stay in sync, and prevents the programmer error when different values are provided to these field pairs. \
Providing either `sumeragi.key_pair` or `sumeragi.peer_id` by hand will result in an error, as it should never be done directly.\n"
)?;
writeln!(writer, "## Default configuration\n")?;
writeln!(
writer,
"The following is the default configuration used by Iroha.\n"
)?;
writeln!(writer, "```json\n{defaults}\n```\n")?;
Self::get_markdown_with_depth(writer, &docs, &mut vec, 2)?;
Ok(())
}

fn get_markdown_with_depth<W: Write>(
writer: &mut W,
docs: &serde_json::Map<String, Value>,
field: &mut Vec<String>,
depth: usize,
) -> color_eyre::Result<()> {
let current_field = {
let mut docs = docs;
for f in &*field {
docs = match &docs[f] {
Value::Object(obj) => obj,
_ => unreachable!(),
};
}
docs
};

for (f, value) in current_field {
field.push(f.clone());
let get_field = field.iter().map(AsRef::as_ref).collect::<Vec<&str>>();
let (doc, inner) = match value {
Value::Object(_) => {
let doc = Self::get_doc_recursive(&get_field)
.expect("Should be there, as already in docs");
(doc.unwrap_or_default(), true)
}
Value::String(s) => (s.clone(), false),
_ => unreachable!("Only strings and objects in docs"),
};
// Hacky workaround to avoid duplicating inner fields docs in the reference
let doc = doc.lines().take(3).collect::<Vec<&str>>().join("\n");
let doc = doc.strip_prefix(' ').unwrap_or(&doc);
let defaults = Self::default()
.get_recursive(get_field)
.expect("Failed to get defaults.");
let defaults = serde_json::to_string_pretty(&defaults)?;
let field_str = field
.join(".")
.chars()
.filter(|&chr| chr != ' ')
.collect::<String>();

write!(writer, "{} `{}`\n\n", "#".repeat(depth), field_str)?;
write!(writer, "{doc}\n\n")?;
write!(writer, "```json\n{defaults}\n```\n\n")?;

if inner {
Self::get_markdown_with_depth(writer, docs, field, depth + 1)?;
}

field.pop();
}
Ok(())
}
}
Loading

0 comments on commit cc9f272

Please sign in to comment.