Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Base CLI parsing #17

Merged
merged 29 commits into from
Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
2b44345
Install `clap` for parsing CLI args.
koxu1996 Feb 2, 2024
22add88
Define deposit/transfer/withdraw subcommands.
koxu1996 Feb 2, 2024
9bc033c
Refactor subcommands and common arguments.
koxu1996 Feb 2, 2024
41ebd06
Run subcommand when matched by name.
koxu1996 Feb 2, 2024
eb04979
Handle subcommand output/error.
koxu1996 Feb 2, 2024
a39b16f
Parse `amount` as u64.
koxu1996 Feb 2, 2024
37109b0
Parse `recipient` and `private_key` as Casper keys.
koxu1996 Feb 5, 2024
1543637
Add tests for CLI usage.
koxu1996 Feb 6, 2024
479a52b
Fix clippy warnings.
koxu1996 Feb 6, 2024
9bc4a71
Use stderr for errors.
koxu1996 Feb 6, 2024
caed7b8
Remove `ClientCommand` trait.
koxu1996 Feb 6, 2024
db60f27
Simplify `fixture_path()` helper.
koxu1996 Feb 6, 2024
70d4d9b
Include fixtures as part of the clean source.
koxu1996 Feb 6, 2024
4e869d2
Remove shallow constructor function.
koxu1996 Feb 7, 2024
e874746
Rename `CasperPublicKey::get()` into `to_bytes()`.
koxu1996 Feb 7, 2024
c91cc30
Move `assert_cmd` into dev dependencies.
koxu1996 Feb 8, 2024
0d154b7
Refactor CLI parsing: `clap` derive + improved errors.
koxu1996 Feb 12, 2024
0bc00c6
Replace type alias with absolute path import.
koxu1996 Feb 13, 2024
13b80c9
Remove unneeded `prase_private_key()`.
koxu1996 Feb 13, 2024
489deb1
Format imports.
koxu1996 Feb 13, 2024
1881742
Refactor constructors of `CasperSigner` and `CasperPrivateKey`.
koxu1996 Feb 13, 2024
a9ed198
Expose signer's field `public_key` directly.
koxu1996 Feb 13, 2024
8eb6e69
Remove `CasperPublicKey::from_key()`.
koxu1996 Feb 13, 2024
55879c2
Remove outdated TODO.
koxu1996 Feb 13, 2024
560645a
Moved core logic into `src` directory.
koxu1996 Feb 13, 2024
455872d
Return unwrapped `ErrorExt` in crypto constructors.
koxu1996 Feb 16, 2024
3775a71
Add tests for parsing Casper public keys.
koxu1996 Feb 29, 2024
1eb91d4
Fix formatting with `cargo fmt`.
koxu1996 Mar 4, 2024
2a37718
Make serialization error context more specific.
koxu1996 Mar 5, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
746 changes: 737 additions & 9 deletions Cargo.lock

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,12 @@
kairosNodeAttrs = {
src = lib.cleanSourceWith {
src = craneLib.path ./.;
filter = path: type: craneLib.filterCargoSources path type;
filter = path: type:
# Allow static files.
(lib.hasInfix "/fixtures/" path) ||
# Default filter (from crane) for .rs files.
(craneLib.filterCargoSources path type)
;
};
nativeBuildInputs = with pkgs; [ pkg-config ];

Expand Down
8 changes: 8 additions & 0 deletions kairos-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,11 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
casper-types = { version = "4.0.1", features = ["std"] } # TODO: Change `std` -> `std-fs-io` in the future version.
clap = { version = "4.4.18", features = ["derive"] }
hex = "0.4.3"
thiserror = "1.0.56"

[dev-dependencies]
assert_cmd = "2.0.13"
predicates = "3.1.0"
30 changes: 29 additions & 1 deletion kairos-cli/bin/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,31 @@
use std::process;

use clap::Parser;
use kairos_cli::commands::{self, Command};

#[derive(Parser)]
#[command(name = "Kairos Client", about = "CLI for interacting with Kairos")]
struct Cli {
#[command(subcommand)]
command: Command,
}

fn main() {
println!("Hello, world!");
let cli = Cli::parse();

let result = match cli.command {
Command::Deposit(args) => commands::deposit::run(args),
Command::Transfer(args) => commands::transfer::run(args),
Command::Withdraw(args) => commands::withdraw::run(args),
};

match result {
Ok(output) => {
println!("{}", output)
}
Err(error) => {
eprintln!("{}", error);
process::exit(1);
}
}
}
26 changes: 26 additions & 0 deletions kairos-cli/src/commands/deposit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
use crate::common::args::{AmountArg, PrivateKeyPathArg};
use crate::crypto::error::CryptoError;
use crate::crypto::signer::CasperSigner;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did we make a custom signer?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to isolate Casper code, to make it easily replaceable in the future. I imagine we might have Signer trait and blockchain specific implementations for it.

use crate::error::CliError;

use clap::Parser;

#[derive(Parser, Debug)]
pub struct Args {
#[clap(flatten)]
amount: AmountArg,
#[clap(flatten)]
private_key_path: PrivateKeyPathArg,
}

pub fn run(args: Args) -> Result<String, CliError> {
let _amount: u64 = args.amount.field;
let _signer =
CasperSigner::from_file(args.private_key_path.field).map_err(CryptoError::from)?;

// TODO: Create transaction and sign it with `signer`.

// TODO: Send transaction to the network, using Rust SDK.

Ok("ok".to_string())
}
15 changes: 15 additions & 0 deletions kairos-cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
pub mod deposit;
pub mod transfer;
pub mod withdraw;

use clap::Subcommand;

#[derive(Subcommand)]
pub enum Command {
#[command(about = "Deposits funds into your account")]
Deposit(deposit::Args),
#[command(about = "Transfers funds to another account")]
Transfer(transfer::Args),
#[command(about = "Withdraws funds from your account")]
Withdraw(withdraw::Args),
}
31 changes: 31 additions & 0 deletions kairos-cli/src/commands/transfer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use crate::common::args::{AmountArg, PrivateKeyPathArg};
use crate::crypto::error::CryptoError;
use crate::crypto::public_key::CasperPublicKey;
use crate::crypto::signer::CasperSigner;
use crate::error::CliError;
use crate::utils::parse_hex_string;

use clap::Parser;

#[derive(Parser)]
pub struct Args {
#[arg(long, short, value_name = "PUBLIC_KEY", value_parser = parse_hex_string)]
recipient: ::std::vec::Vec<u8>, // Absolute path is required here - see https://github.com/clap-rs/clap/issues/4626#issue-1528622454.
#[clap(flatten)]
amount: AmountArg,
#[clap(flatten)]
private_key_path: PrivateKeyPathArg,
}

pub fn run(args: Args) -> Result<String, CliError> {
let _recipient = CasperPublicKey::from_bytes(args.recipient.as_ref())?;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So... do we have a test that public keys on https://cspr.live/ parse using this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests added in 3775a71.

let _amount: u64 = args.amount.field;
let _signer =
CasperSigner::from_file(args.private_key_path.field).map_err(CryptoError::from)?;

// TODO: Create transaction and sign it with `signer`.

// TODO: Send transaction to the network, using Rust SDK.

Ok("ok".to_string())
}
26 changes: 26 additions & 0 deletions kairos-cli/src/commands/withdraw.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
use crate::common::args::{AmountArg, PrivateKeyPathArg};
use crate::crypto::error::CryptoError;
use crate::crypto::signer::CasperSigner;
use crate::error::CliError;

use clap::Parser;

#[derive(Parser)]
pub struct Args {
#[clap(flatten)]
amount: AmountArg,
#[clap(flatten)]
private_key_path: PrivateKeyPathArg,
}

pub fn run(args: Args) -> Result<String, CliError> {
let _amount: u64 = args.amount.field;
let _signer =
CasperSigner::from_file(args.private_key_path.field).map_err(CryptoError::from)?;

// TODO: Create transaction and sign it with `signer`.

// TODO: Send transaction to the network, using Rust SDK.

Ok("ok".to_string())
}
15 changes: 15 additions & 0 deletions kairos-cli/src/common/args.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use std::path::PathBuf;

use clap::Args;

#[derive(Args, Debug)]
pub struct AmountArg {
#[arg(name = "amount", long, short, value_name = "NUM_MOTES")]
pub field: u64,
}

#[derive(Args, Debug)]
pub struct PrivateKeyPathArg {
#[arg(name = "private-key", long, short = 'k', value_name = "FILE_PATH")]
pub field: PathBuf,
}
1 change: 1 addition & 0 deletions kairos-cli/src/common/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod args;
15 changes: 15 additions & 0 deletions kairos-cli/src/crypto/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use casper_types::ErrorExt;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum CryptoError {
/// Failed to parse a public key from a raw data.
#[error("failed to parse private key: {error}")]
FailedToParseKey {
#[from]
error: ErrorExt,
},
/// Invalid public key (hexdigest) or other encoding related error.
#[error("failed to serialize/deserialize '{context}'")]
Serialization { context: &'static str },
}
4 changes: 4 additions & 0 deletions kairos-cli/src/crypto/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub mod error;
pub mod private_key;
pub mod public_key;
pub mod signer;
11 changes: 11 additions & 0 deletions kairos-cli/src/crypto/private_key.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use std::path::Path;

use casper_types::ErrorExt;

pub struct CasperPrivateKey(pub casper_types::SecretKey);

impl CasperPrivateKey {
pub fn from_file<P: AsRef<Path>>(file_path: P) -> Result<Self, ErrorExt> {
casper_types::SecretKey::from_file(file_path).map(Self)
}
}
68 changes: 68 additions & 0 deletions kairos-cli/src/crypto/public_key.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use crate::crypto::error::CryptoError;
use casper_types::bytesrepr::{FromBytes, ToBytes};

#[derive(Clone)]
pub struct CasperPublicKey(pub casper_types::PublicKey);

impl CasperPublicKey {
pub fn from_bytes(bytes: &[u8]) -> Result<Self, CryptoError> {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don’t make this part of the inherent implementation, just impl the FromBytes trait if you want to do this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FromBytes is a trait existing in casper_types, and my intention was to make Kairos as much independent as possible.

let (public_key, _remainder) =
casper_types::PublicKey::from_bytes(bytes).map_err(|_e| {
CryptoError::Serialization {
context: "public key serialization",
}
})?;
Ok(Self(public_key))
}

#[allow(unused)]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just implement the ToBytes trait, rather than doing this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as with FromBytes - this would make Kairos more entangled with Casper's code 🙄. I could reimplement ToBytes trait, but it does not seem useful at this point.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... but if you aren't using it, do we need it for anything?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not yet. However, it will be useful once we have signature verification implemented - #7.

fn to_bytes(&self) -> Result<Vec<u8>, CryptoError> {
self.0.to_bytes().map_err(|_e| CryptoError::Serialization {
context: "public key deserialization",
})
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_casper_ed25519_public_key() {
// This public key has a 01 prefix indicating Ed25519.
let bytes =
hex::decode("01c377281132044bd3278b039925eeb3efdb9d99dd5f46d9ec6a764add34581af7")
.unwrap();
let result = CasperPublicKey::from_bytes(&bytes);
assert!(
result.is_ok(),
"Ed25519 public key should be parsed correctly"
);
}

#[test]
fn test_casper_secp256k1_public_key() {
// This public key has a 02 prefix indicating Secp256k1.
let bytes =
hex::decode("0202e99759649fa63a72c685b72e696b30c90f1deabb02d0d9b1de45eb371a73e5bb")
.unwrap();
let result = CasperPublicKey::from_bytes(&bytes);
assert!(
result.is_ok(),
"Secp256k1 public key should be parsed correctly"
);
}

#[test]
fn test_casper_unrecognized_prefix() {
// Using a 99 prefix which is not recognized.
let bytes =
hex::decode("99c377281132044bd3278b039925eeb3efdb9d99dd5f46d9ec6a764add34581af7")
.unwrap();
let result = CasperPublicKey::from_bytes(&bytes);
assert!(
result.is_err(),
"Unrecognized prefix should result in an error"
);
}
}
38 changes: 38 additions & 0 deletions kairos-cli/src/crypto/signer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use std::path::Path;

use super::private_key::CasperPrivateKey;
use super::public_key::CasperPublicKey;
use crate::crypto::error::CryptoError;
use casper_types::bytesrepr::ToBytes;
use casper_types::{crypto, ErrorExt, PublicKey};

pub struct CasperSigner {
secret_key: CasperPrivateKey,
pub public_key: CasperPublicKey,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to have a public key here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Public key is necessary for signing data with Casper signer:

pub fn sign<T: AsRef<[u8]>>(
    message: T,
    secret_key: &SecretKey,
    public_key: &PublicKey
) -> Signature

Since we have user's private key, we can derive public key - we could do that every time sign_message() is called, but I thought storing it might come handy soon.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh... don't use that signer, it has an unused public key for some reason.

Use this instead:

pub fn sign<T: AsRef<[u8]>>(secret_key: &SecretKey, message: T) -> Signature {
    match secret_key {
        SecretKey::System => Signature::System,
        SecretKey::Ed25519(secret_key) => {
            let signature = secret_key.sign(message.as_ref());
            Signature::Ed25519(signature)
        }
        SecretKey::Secp256k1(secret_key) => {
            let signature = secret_key
                .try_sign(message.as_ref())
                .expect("should create signature");
            Signature::Secp256k1(signature)
        }
        _ => panic!("SecretKey is marked as non-exhaustive, but this should never happen"),
    }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need public key anyway (for signature verification), so I think there is no reason to reimplement Casper's sign() method just to avoid using it.

}

#[allow(unused)]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this used though?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not used, because we have no signature verification yet:

tracing::info!("TODO: verifying transfer signature");

This is about to come soon - once we establish transaction format (I am currently exploring protobuffs and ASN.1+DER).

impl CasperSigner {
pub fn from_file<P: AsRef<Path>>(file_path: P) -> Result<Self, ErrorExt> {
let secret_key = CasperPrivateKey::from_file(file_path)?;

// Derive the public key.
let public_key = CasperPublicKey(PublicKey::from(&secret_key.0));
quinn-dougherty marked this conversation as resolved.
Show resolved Hide resolved

Ok(CasperSigner {
secret_key,
public_key,
})
}

pub fn sign_message(&self, message: &[u8]) -> Result<Vec<u8>, CryptoError> {
let signature = crypto::sign(message, &self.secret_key.0, &self.public_key.0);
let bytes = signature
.to_bytes()
.map_err(|_e| CryptoError::Serialization {
context: "signature",
})?;

Ok(bytes)
}
}
20 changes: 20 additions & 0 deletions kairos-cli/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use hex::FromHexError;
use thiserror::Error;

use crate::crypto::error::CryptoError;

#[derive(Error, Debug)]
pub enum CliError {
/// Cryptography error.
#[error("cryptography error: {error}")]
CryptoError {
#[from]
error: CryptoError,
},
/// Failed to parse hex string.
#[error("failed to parse hex string: {error}")]
ParseError {
#[from]
error: FromHexError,
},
}
5 changes: 5 additions & 0 deletions kairos-cli/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pub mod commands;
pub mod common;
pub mod crypto;
pub mod error;
pub mod utils;
6 changes: 6 additions & 0 deletions kairos-cli/src/utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
use crate::error::CliError;

/// Custom parser function to convert a hexadecimal string to a byte array.
pub fn parse_hex_string(s: &str) -> Result<Vec<u8>, CliError> {
hex::decode(s).map_err(|e| e.into())
}
Loading
Loading