From 2ebdb15f3e3eba3f8e76b970a9fb1a3c871ff4f6 Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli Date: Wed, 14 Aug 2024 11:50:35 +0200 Subject: [PATCH 1/9] fix and improve tx dump --- crates/sdk/src/tx.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/sdk/src/tx.rs b/crates/sdk/src/tx.rs index ce5758d959..5c9d011cdf 100644 --- a/crates/sdk/src/tx.rs +++ b/crates/sdk/src/tx.rs @@ -190,14 +190,12 @@ impl ProcessTxResponse { /// Build and dump a transaction either to file or to screen pub fn dump_tx(io: &IO, args: &args::Tx, tx: Tx) { - let tx_id = tx.header_hash(); - let serialized_tx = tx.serialize(); - match args.output_folder.to_owned() { + match args.output_folder.clone() { Some(path) => { - let tx_filename = format!("{}.tx", tx_id); - let tx_path = path.join(tx_filename); - let out = File::create(&tx_path).unwrap(); - serde_json::to_writer_pretty(out, &serialized_tx) + let tx_path = path.join(format!("{}.tx", tx.header_hash())); + let out = File::create(&tx_path) + .expect("Should be able to create a file to dump tx"); + serde_json::to_writer_pretty(out, &tx) .expect("Should be able to write to file."); display_line!( io, @@ -206,6 +204,8 @@ pub fn dump_tx(io: &IO, args: &args::Tx, tx: Tx) { ); } None => { + let serialized_tx = serde_json::to_string_pretty(&tx) + .expect("Should be able to json encode the tx."); display_line!(io, "Below the serialized transaction: \n"); display_line!(io, "{}", serialized_tx) } From 0ab35f7054778d2c1f33a449b56f3eb9e527ec46 Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli Date: Wed, 14 Aug 2024 16:56:13 +0200 Subject: [PATCH 2/9] fix deserialization --- crates/tx/src/types.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/crates/tx/src/types.rs b/crates/tx/src/types.rs index 883dc03658..814b413b28 100644 --- a/crates/tx/src/types.rs +++ b/crates/tx/src/types.rs @@ -1188,14 +1188,10 @@ impl Tx { HEXUPPER.encode(&tx_bytes) } - /// Deserialize from hex encoding + /// Deserialize tx from json pub fn deserialize(data: &[u8]) -> Result { - if let Ok(hex) = serde_json::from_slice::(data) { - match HEXUPPER.decode(hex.as_bytes()) { - Ok(bytes) => Tx::try_from_slice(&bytes) - .map_err(DecodeError::InvalidEncoding), - Err(e) => Err(DecodeError::InvalidHex(e)), - } + if let Ok(tx) = serde_json::from_slice::(data) { + Ok(tx) } else { Err(DecodeError::InvalidJsonString) } From 54921b485a75f4b0d853ef52ab4b9a3fcb9b7446 Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli Date: Fri, 16 Aug 2024 10:28:57 +0200 Subject: [PATCH 3/9] fix/improve signature dump --- Cargo.lock | 16 ++++------------ crates/apps_lib/src/client/tx.rs | 14 +++++++------- crates/sdk/src/signing.rs | 3 ++- crates/sdk/src/tx.rs | 5 ++++- crates/tx/src/sign.rs | 24 +----------------------- 5 files changed, 18 insertions(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 379342f7e2..0aab5e5b4e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5786,12 +5786,6 @@ dependencies = [ "num-traits 0.2.17", ] -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - [[package]] name = "num-derive" version = "0.3.3" @@ -8359,13 +8353,12 @@ dependencies = [ [[package]] name = "time" -version = "0.3.36" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" dependencies = [ "deranged", "itoa", - "num-conv", "powerfmt", "serde", "time-core", @@ -8380,11 +8373,10 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.18" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" dependencies = [ - "num-conv", "time-core", ] diff --git a/crates/apps_lib/src/client/tx.rs b/crates/apps_lib/src/client/tx.rs index 3719951e35..eb474a1686 100644 --- a/crates/apps_lib/src/client/tx.rs +++ b/crates/apps_lib/src/client/tx.rs @@ -1071,8 +1071,11 @@ where transaction } else { edisplay_line!(namada.io(), "Couldn't decode the transaction."); - safe_exit(1) + return Err(error::Error::Other(format!( + "Couldn't decode the transaction." + ))); }; + let default_signer = Some(owner.clone()); let signing_data = aux_signing_data( namada, @@ -1110,15 +1113,11 @@ where Some(path) => path.join(filename), None => filename.into(), }; - let signature_path = File::create(&output_path) .expect("Should be able to create signature file."); + serde_json::to_writer_pretty(signature_path, &signature) + .expect("Signature should be deserializable."); - serde_json::to_writer_pretty( - signature_path, - &signature.serialize(), - ) - .expect("Signature should be deserializable."); display_line!( namada.io(), "Signature for {} serialized at {}", @@ -1127,6 +1126,7 @@ where ); } } + Ok(()) } diff --git a/crates/sdk/src/signing.rs b/crates/sdk/src/signing.rs index 1eab0842de..7c180ec9bc 100644 --- a/crates/sdk/src/signing.rs +++ b/crates/sdk/src/signing.rs @@ -215,7 +215,8 @@ where .signatures .iter() .map(|bytes| { - let sigidx = SignatureIndex::deserialize(bytes).unwrap(); + let sigidx = + serde_json::from_slice::(bytes).unwrap(); used_pubkeys.insert(sigidx.pubkey.clone()); sigidx }) diff --git a/crates/sdk/src/tx.rs b/crates/sdk/src/tx.rs index 5c9d011cdf..42fe49564c 100644 --- a/crates/sdk/src/tx.rs +++ b/crates/sdk/src/tx.rs @@ -192,7 +192,10 @@ impl ProcessTxResponse { pub fn dump_tx(io: &IO, args: &args::Tx, tx: Tx) { match args.output_folder.clone() { Some(path) => { - let tx_path = path.join(format!("{}.tx", tx.header_hash())); + let tx_path = path.join(format!( + "{}.tx", + tx.header_hash().to_string().to_lowercase() + )); let out = File::create(&tx_path) .expect("Should be able to create a file to dump tx"); serde_json::to_writer_pretty(out, &tx) diff --git a/crates/tx/src/sign.rs b/crates/tx/src/sign.rs index 4120b60e00..296778e0ae 100644 --- a/crates/tx/src/sign.rs +++ b/crates/tx/src/sign.rs @@ -2,11 +2,8 @@ use std::cmp::Ordering; -use data_encoding::HEXUPPER; use namada_core::address::Address; -use namada_core::borsh::{ - BorshDeserialize, BorshSchema, BorshSerialize, BorshSerializeExt, -}; +use namada_core::borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use namada_core::key::common; use namada_macros::BorshDeserializer; #[cfg(feature = "migrations")] @@ -64,25 +61,6 @@ impl SignatureIndex { pub fn to_vec(&self) -> Vec { vec![self.clone()] } - - /// Serialize as a string. - pub fn serialize(&self) -> String { - let signature_bytes = self.serialize_to_vec(); - HEXUPPER.encode(&signature_bytes) - } - - /// Deserialize from a string slice - pub fn deserialize(data: &[u8]) -> Result { - if let Ok(hex) = serde_json::from_slice::(data) { - match HEXUPPER.decode(hex.as_bytes()) { - Ok(bytes) => Self::try_from_slice(&bytes) - .map_err(SigIndexDecodeError::Encoding), - Err(e) => Err(SigIndexDecodeError::Hex(e)), - } - } else { - Err(SigIndexDecodeError::JsonString) - } - } } impl Ord for SignatureIndex { From c651ba0da3e7efa26dcc25daadb263738e560003 Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli Date: Fri, 16 Aug 2024 12:15:15 +0200 Subject: [PATCH 4/9] improve tx serialization --- crates/apps_lib/src/client/tx.rs | 6 +-- crates/core/src/hash.rs | 47 ++++++++++++++++-- crates/tx/src/lib.rs | 84 ++++++++++++++++++++++++++++++++ crates/tx/src/types.rs | 15 +++--- 4 files changed, 140 insertions(+), 12 deletions(-) diff --git a/crates/apps_lib/src/client/tx.rs b/crates/apps_lib/src/client/tx.rs index eb474a1686..2324d76429 100644 --- a/crates/apps_lib/src/client/tx.rs +++ b/crates/apps_lib/src/client/tx.rs @@ -1071,9 +1071,9 @@ where transaction } else { edisplay_line!(namada.io(), "Couldn't decode the transaction."); - return Err(error::Error::Other(format!( - "Couldn't decode the transaction." - ))); + return Err(error::Error::Other( + "Couldn't decode the transaction.".to_string(), + )); }; let default_signer = Some(owner.clone()); diff --git a/crates/core/src/hash.rs b/crates/core/src/hash.rs index a2cfa4112c..bfddb087bd 100644 --- a/crates/core/src/hash.rs +++ b/crates/core/src/hash.rs @@ -10,7 +10,7 @@ use data_encoding::HEXUPPER; use namada_macros::BorshDeserializer; #[cfg(feature = "migrations")] use namada_migrations::*; -use serde::{Deserialize, Serialize}; +use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use sha2::{Digest, Sha256}; use thiserror::Error; @@ -51,12 +51,53 @@ pub type HashResult = std::result::Result; BorshDeserialize, BorshDeserializer, BorshSchema, - Serialize, - Deserialize, )] /// A hash, typically a sha-2 hash of a tx pub struct Hash(pub [u8; HASH_LENGTH]); +impl Serialize for Hash { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + // Convert the byte array to a hex string + let hex_string = HEXUPPER.encode(&self.0); + // Serialize the hex string + serializer.serialize_str(&hex_string) + } +} + +impl<'de> Deserialize<'de> for Hash { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + // Deserialize a hex string + let hex_string = + ::deserialize(deserializer)?; + // Convert the hex string back to a byte array + let bytes = HEXUPPER + .decode(hex_string.as_bytes()) + .map_err(de::Error::custom)?; + + // Ensure the byte array has the correct length + if bytes.len() != HASH_LENGTH { + return Err(de::Error::custom(format!( + "Invalid length: expected {} bytes, got {}", + HASH_LENGTH, + bytes.len() + ))); + } + + let mut output = [0u8; HASH_LENGTH]; + HEXUPPER + .decode_mut(hex_string.as_bytes(), &mut output) + .expect("Hash decoding shouldn't fail"); + + Ok(Hash(output)) + } +} + impl arse_merkle_tree::traits::Value for Hash { fn as_slice(&self) -> &[u8] { self.0.as_slice() diff --git a/crates/tx/src/lib.rs b/crates/tx/src/lib.rs index 93402cd1cb..de7406595d 100644 --- a/crates/tx/src/lib.rs +++ b/crates/tx/src/lib.rs @@ -36,6 +36,90 @@ pub use types::{ Signer, Tx, TxCommitments, TxError, VerifySigError, }; +/// Length of the transaction sections salt +pub const SALT_LENGTH: usize = 8; + +#[allow(missing_docs)] +mod hex_salt_serde { + use data_encoding::HEXUPPER; + use serde::{de, Deserializer, Serializer}; + + use super::*; + + pub fn serialize( + salt: &[u8; SALT_LENGTH], + serializer: S, + ) -> Result + where + S: Serializer, + { + // Convert the byte array to a hex string + let hex_string = HEXUPPER.encode(salt); + serializer.serialize_str(&hex_string) + } + + pub fn deserialize<'de, D>( + deserializer: D, + ) -> Result<[u8; SALT_LENGTH], D::Error> + where + D: Deserializer<'de>, + { + // Deserialize a hex string + let hex_string = + ::deserialize(deserializer)?; + // Convert the hex string back to a byte array + let bytes = HEXUPPER + .decode(hex_string.as_bytes()) + .map_err(de::Error::custom)?; + + if bytes.len() != SALT_LENGTH { + return Err(de::Error::custom(format!( + "Invalid length: expected {} bytes, got {}", + SALT_LENGTH, + bytes.len() + ))); + } + + let mut output = [0u8; SALT_LENGTH]; + HEXUPPER + .decode_mut(hex_string.as_bytes(), &mut output) + .expect("Hash decoding shouldn't fail"); + + Ok(output) + } +} + +#[allow(missing_docs)] +mod hex_data_serde { + use data_encoding::HEXUPPER; + use serde::{de, Deserializer, Serializer}; + + pub fn serialize( + #[allow(clippy::ptr_arg)] data: &Vec, + serializer: S, + ) -> Result + where + S: Serializer, + { + // Convert the byte array to a hex string + let hex_string = HEXUPPER.encode(data); + serializer.serialize_str(&hex_string) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + // Deserialize a hex string + let hex_string = + ::deserialize(deserializer)?; + // Convert the hex string back to a byte array + HEXUPPER + .decode(hex_string.as_bytes()) + .map_err(de::Error::custom) + } +} + #[cfg(test)] mod tests { use data_encoding::HEXLOWER; diff --git a/crates/tx/src/types.rs b/crates/tx/src/types.rs index 814b413b28..f2dbdab784 100644 --- a/crates/tx/src/types.rs +++ b/crates/tx/src/types.rs @@ -32,8 +32,8 @@ use thiserror::Error; use crate::data::protocol::ProtocolTx; use crate::data::{hash_tx, Fee, GasLimit, TxType, WrapperTx}; -use crate::proto; use crate::sign::SignatureIndex; +use crate::{hex_data_serde, hex_salt_serde, proto, SALT_LENGTH}; /// Represents an error in signature verification #[allow(missing_docs)] @@ -232,8 +232,10 @@ pub fn verify_standalone_sig>( )] pub struct Data { /// Salt with additional random data (usually a timestamp) - pub salt: [u8; 8], + #[serde(with = "hex_salt_serde")] + pub salt: [u8; SALT_LENGTH], /// Data bytes + #[serde(with = "hex_data_serde")] pub data: Vec, } @@ -250,7 +252,7 @@ impl Data { Self { salt: { - let mut buf = [0; 8]; + let mut buf = [0; SALT_LENGTH]; OsRng.fill_bytes(&mut buf); buf }, @@ -349,7 +351,8 @@ impl Commitment { )] pub struct Code { /// Additional random data - pub salt: [u8; 8], + #[serde(with = "hex_salt_serde")] + pub salt: [u8; SALT_LENGTH], /// Actual transaction code pub code: Commitment, /// The tag for the transaction code @@ -369,7 +372,7 @@ impl Code { Self { salt: { - let mut buf = [0; 8]; + let mut buf = [0; SALT_LENGTH]; OsRng.fill_bytes(&mut buf); buf }, @@ -387,7 +390,7 @@ impl Code { Self { salt: { - let mut buf = [0; 8]; + let mut buf = [0; SALT_LENGTH]; OsRng.fill_bytes(&mut buf); buf }, From 30439a0d300394b7e39be511cb3605f5539c693a Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli Date: Mon, 26 Aug 2024 15:50:41 +0200 Subject: [PATCH 5/9] added offline sign utils, improve signature serialization --- crates/apps_lib/src/cli.rs | 68 +++++++++++++++++++++++++++++ crates/apps_lib/src/cli/client.rs | 3 ++ crates/apps_lib/src/client/utils.rs | 61 ++++++++++++++++++++++++++ crates/core/src/key/common.rs | 55 ++++++++++++++++++++--- crates/core/src/key/secp256k1.rs | 2 +- 5 files changed, 183 insertions(+), 6 deletions(-) diff --git a/crates/apps_lib/src/cli.rs b/crates/apps_lib/src/cli.rs index 7124945d0f..9daa31ecf8 100644 --- a/crates/apps_lib/src/cli.rs +++ b/crates/apps_lib/src/cli.rs @@ -2458,6 +2458,7 @@ pub mod cmds { InitGenesisEstablishedAccount(InitGenesisEstablishedAccount), InitGenesisValidator(InitGenesisValidator), PkToTmAddress(PkToTmAddress), + SignOffline(SignOffline), DefaultBaseDir(DefaultBaseDir), EpochSleep(EpochSleep), ValidateGenesisTemplates(ValidateGenesisTemplates), @@ -2486,6 +2487,8 @@ pub mod cmds { SubCmd::parse(matches).map(Self::InitGenesisValidator); let pk_to_tm_address = SubCmd::parse(matches).map(Self::PkToTmAddress); + let sign_offline = + SubCmd::parse(matches).map(Self::SignOffline); let default_base_dir = SubCmd::parse(matches).map(Self::DefaultBaseDir); let epoch_sleep = SubCmd::parse(matches).map(Self::EpochSleep); @@ -2508,6 +2511,7 @@ pub mod cmds { .or(validate_genesis_templates) .or(genesis_tx) .or(parse_migrations_json) + .or(sign_offline) }) } @@ -2522,6 +2526,7 @@ pub mod cmds { .subcommand(InitGenesisEstablishedAccount::def()) .subcommand(InitGenesisValidator::def()) .subcommand(PkToTmAddress::def()) + .subcommand(SignOffline::def()) .subcommand(DefaultBaseDir::def()) .subcommand(EpochSleep::def()) .subcommand(ValidateGenesisTemplates::def()) @@ -3189,6 +3194,25 @@ pub mod cmds { } } + #[derive(Clone, Debug)] + pub struct SignOffline(pub args::SignOffline); + + impl SubCmd for SignOffline { + const CMD: &'static str = "sign-offline"; + + fn parse(matches: &ArgMatches) -> Option { + matches + .subcommand_matches(Self::CMD) + .map(|matches| Self(args::SignOffline::parse(matches))) + } + + fn def() -> App { + App::new(Self::CMD) + .about(wrap!("Offlne sign a transaction.")) + .add_args::() + } + } + #[derive(Clone, Debug)] pub struct DefaultBaseDir(pub args::DefaultBaseDir); @@ -3444,6 +3468,8 @@ pub mod args { DefaultFn(|| PortId::from_str("transfer").unwrap()), ); pub const PRE_GENESIS: ArgFlag = flag("pre-genesis"); + pub const PRIVATE_KEYS: ArgMulti = + arg_multi("secret-keys"); pub const PROPOSAL_PGF_STEWARD: ArgFlag = flag("pgf-stewards"); pub const PROPOSAL_PGF_FUNDING: ArgFlag = flag("pgf-funding"); pub const PROTOCOL_KEY: ArgOpt = arg_opt("protocol-key"); @@ -8036,6 +8062,48 @@ pub mod args { } } + #[derive(Clone, Debug)] + pub struct SignOffline { + pub tx_path: PathBuf, + pub secret_keys: Vec, + pub owner: Address, + pub output_folder_path: Option, + } + + impl Args for SignOffline { + fn parse(matches: &ArgMatches) -> Self { + let tx_path = DATA_PATH.parse(matches); + let secret_keys = PRIVATE_KEYS.parse(matches); + let owner = RAW_ADDRESS.parse(matches); + let output_folder_path = OUTPUT_FOLDER_PATH.parse(matches); + + Self { + tx_path, + secret_keys, + owner, + output_folder_path, + } + } + + fn def(app: App) -> App { + app.arg( + DATA_PATH + .def() + .help(wrap!("The path to the serialized transaction.")), + ) + .arg(PRIVATE_KEYS.def().help(wrap!( + "The set of private keys to use to sign the transaction. The \ + order matters." + ))) + .arg(RAW_ADDRESS.def().help(wrap!("The owner's address."))) + .arg( + OUTPUT_FOLDER_PATH + .def() + .help("Folder to where serialize the signatures"), + ) + } + } + #[derive(Clone, Debug)] pub struct DefaultBaseDir {} diff --git a/crates/apps_lib/src/cli/client.rs b/crates/apps_lib/src/cli/client.rs index fb708d235f..0c26239996 100644 --- a/crates/apps_lib/src/cli/client.rs +++ b/crates/apps_lib/src/cli/client.rs @@ -816,6 +816,9 @@ impl CliApi { ClientUtils::PkToTmAddress(PkToTmAddress(args)) => { utils::pk_to_tm_address(global_args, args) } + ClientUtils::SignOffline(SignOffline(args)) => { + utils::sign_offline(global_args, args).await + } ClientUtils::DefaultBaseDir(DefaultBaseDir(args)) => { utils::default_base_dir(global_args, args) } diff --git a/crates/apps_lib/src/client/utils.rs b/crates/apps_lib/src/client/utils.rs index 53826e30f0..95f54c4ce7 100644 --- a/crates/apps_lib/src/client/utils.rs +++ b/crates/apps_lib/src/client/utils.rs @@ -9,6 +9,7 @@ use flate2::read::GzDecoder; use flate2::write::GzEncoder; use flate2::Compression; use itertools::Either; +use namada_sdk::account::AccountPublicKeysMap; use namada_sdk::address::Address; use namada_sdk::args::DeviceTransport; use namada_sdk::chain::ChainId; @@ -16,6 +17,7 @@ use namada_sdk::dec::Dec; use namada_sdk::key::*; use namada_sdk::string_encoding::StringEncoded; use namada_sdk::token; +use namada_sdk::tx::Tx; use namada_sdk::uint::Uint; use namada_sdk::wallet::{alias, LoadStoreError, Wallet}; use namada_vm::validate_untrusted_wasm; @@ -1031,6 +1033,65 @@ pub async fn sign_genesis_tx( } } +/// Offline sign a transactions. +pub async fn sign_offline( + _global_args: args::Global, + args::SignOffline { + tx_path, + secret_keys, + owner, + output_folder_path, + }: args::SignOffline, +) { + let tx_data = if let Ok(tx_data) = fs::read(&tx_path) { + tx_data + } else { + eprintln!("Couldn't open file at {}", tx_path.display()); + safe_exit(1) + }; + + let tx = if let Ok(transaction) = Tx::deserialize(tx_data.as_ref()) { + transaction + } else { + eprintln!("Couldn't decode the transaction."); + safe_exit(1) + }; + + let account_public_keys_map = AccountPublicKeysMap::from_iter( + secret_keys.iter().map(|sk| sk.to_public()), + ); + + let signatures = tx.compute_section_signature( + &secret_keys, + &account_public_keys_map, + Some(owner), + ); + + for signature in &signatures { + let filename = format!( + "offline_signature_{}_{}.sig", + tx.header_hash().to_string().to_lowercase(), + signature.pubkey, + ); + + let tx_path = match output_folder_path { + Some(ref path) => path.join(filename).to_string_lossy().to_string(), + None => filename, + }; + + let signature_path = File::create(&tx_path) + .expect("Should be able to create signature file."); + + serde_json::to_writer_pretty(signature_path, &signature) + .expect("Signature should be deserializable."); + + println!( + "Signature for {} serialized at {}", + signature.pubkey, tx_path + ); + } +} + /// Add a spinning wheel to a message for long running commands. /// Can be turned off for E2E tests by setting the `REDUCED_CLI_PRINTING` /// environment variable. diff --git a/crates/core/src/key/common.rs b/crates/core/src/key/common.rs index 4c6788237a..1944397165 100644 --- a/crates/core/src/key/common.rs +++ b/crates/core/src/key/common.rs @@ -1,16 +1,17 @@ //! Cryptographic keys -use std::fmt::Display; +use std::fmt::{self, Display}; use std::str::FromStr; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use borsh_ext::BorshSerializeExt; -use data_encoding::HEXLOWER; +use data_encoding::{HEXLOWER, HEXUPPER}; use namada_macros::BorshDeserializer; #[cfg(feature = "migrations")] use namada_migrations::*; #[cfg(any(test, feature = "rand"))] use rand::{CryptoRng, RngCore}; -use serde::{Deserialize, Serialize}; +use serde::de::{self, Visitor}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use thiserror::Error; use super::{ @@ -309,8 +310,6 @@ impl FromStr for SecretKey { PartialOrd, Ord, Hash, - Serialize, - Deserialize, BorshSerialize, BorshDeserialize, BorshDeserializer, @@ -343,6 +342,52 @@ impl string_encoding::Format for Signature { } } +impl Serialize for CommonSignature { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let hex_str = HEXUPPER.encode(&self.serialize_to_vec()); + serializer.serialize_str(&hex_str) + } +} + +// Implement custom deserialization +impl<'de> Deserialize<'de> for CommonSignature { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct SignatureVisitor; + + impl<'de> Visitor<'de> for SignatureVisitor { + type Value = CommonSignature; + + fn expecting( + &self, + formatter: &mut fmt::Formatter<'_>, + ) -> fmt::Result { + formatter.write_str( + "a hex string representing either an Ed25519 or Secp256k1 \ + signature", + ) + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + let bytes = + HEXUPPER.decode(value.as_bytes()).map_err(E::custom)?; + CommonSignature::try_from_slice(&bytes) + .map_err(|e| E::custom(e.to_string())) + } + } + + deserializer.deserialize_str(SignatureVisitor) + } +} + impl_display_and_from_str_via_format!(Signature); impl From for Signature { diff --git a/crates/core/src/key/secp256k1.rs b/crates/core/src/key/secp256k1.rs index 5e3747da22..787b448935 100644 --- a/crates/core/src/key/secp256k1.rs +++ b/crates/core/src/key/secp256k1.rs @@ -270,7 +270,7 @@ impl BorshSchema for SecretKey { impl Display for SecretKey { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", HEXLOWER.encode(&self.0.to_bytes())) + write!(f, "{}", HEXLOWER.encode(self.0.to_bytes().as_ref())) } } From a76630c592c5b3944b31076b61c1f595342bbdf8 Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli Date: Tue, 27 Aug 2024 12:15:27 +0200 Subject: [PATCH 6/9] added e2e test --- .github/workflows/scripts/e2e.json | 5 +- crates/tests/src/e2e/helpers.rs | 27 ++++++- crates/tests/src/e2e/ledger_tests.rs | 103 ++++++++++++++++++++++++++- 3 files changed, 128 insertions(+), 7 deletions(-) diff --git a/.github/workflows/scripts/e2e.json b/.github/workflows/scripts/e2e.json index 163a87fa99..b8fc16f2d2 100644 --- a/.github/workflows/scripts/e2e.json +++ b/.github/workflows/scripts/e2e.json @@ -29,5 +29,6 @@ "e2e::wallet_tests::wallet_encrypted_key_cmds": 1, "e2e::wallet_tests::wallet_encrypted_key_cmds_env_var": 1, "e2e::wallet_tests::wallet_unencrypted_key_cmds": 1, - "e2e::ledger_tests::masp_txs_and_queries": 82 -} + "e2e::ledger_tests::masp_txs_and_queries": 82, + "e2e::ledger_tests::offline_sign": 20 +} \ No newline at end of file diff --git a/crates/tests/src/e2e/helpers.rs b/crates/tests/src/e2e/helpers.rs index 381c2e57e1..80d77a7cb5 100644 --- a/crates/tests/src/e2e/helpers.rs +++ b/crates/tests/src/e2e/helpers.rs @@ -1,9 +1,9 @@ //! E2E test helpers -use std::fs::{File, OpenOptions}; +use std::fs::{self, File, OpenOptions}; use std::future::Future; use std::io::Write; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::process::Command; use std::str::FromStr; use std::time::{Duration, Instant}; @@ -222,7 +222,7 @@ pub fn find_keypair( .unwrap() .1; let key = format!("{}{}", sk, pk); - common::SecretKey::from_str(&key).map_err(|e| { + common::SecretKey::from_str(sk).map_err(|e| { eyre!(format!( "Key: {} parsed from {}, Error: {}\n\nOutput: {}", key, matched, e, unread @@ -726,3 +726,24 @@ pub fn get_gaia_gov_address(test: &Test) -> Result { Ok(matched.trim().to_string()) } + +pub fn find_offline_file( + dir: &Path, + extension: &str, +) -> Result> { + // Read the directory entries + for entry in fs::read_dir(dir)? { + let entry = entry?; + let path = entry.path(); + + if path.is_file() { + if let Some(file_extension) = path.extension() { + if file_extension == extension { + return Ok(Some(path)); + } + } + } + } + + Ok(None) +} diff --git a/crates/tests/src/e2e/ledger_tests.rs b/crates/tests/src/e2e/ledger_tests.rs index 160338c268..923606c878 100644 --- a/crates/tests/src/e2e/ledger_tests.rs +++ b/crates/tests/src/e2e/ledger_tests.rs @@ -38,8 +38,9 @@ use setup::constants::*; use setup::Test; use super::helpers::{ - epochs_per_year_from_min_duration, get_height, get_pregenesis_wallet, - wait_for_block_height, wait_for_wasm_pre_compile, + epochs_per_year_from_min_duration, find_keypair, find_offline_file, + get_height, get_pregenesis_wallet, wait_for_block_height, + wait_for_wasm_pre_compile, }; use super::setup::{set_ethereum_bridge_mode, working_dir, NamadaCmd}; use crate::e2e::helpers::{ @@ -123,6 +124,104 @@ fn run_ledger() -> Result<()> { Ok(()) } +#[test] +fn offline_sign() -> Result<()> { + let test = setup::single_node_net()?; + + set_ethereum_bridge_mode( + &test, + &test.net.chain_id, + Who::Validator(0), + ethereum_bridge::ledger::Mode::Off, + None, + ); + + // 1. Run the ledger node + let mut ledger = + start_namada_ledger_node_wait_wasm(&test, Some(0), Some(40))?; + ledger.exp_string("Committed block hash")?; + let _bg_ledger = ledger.background(); + + let validator_one_rpc = get_actor_rpc(&test, Who::Validator(0)); + + let output_folder = test.test_dir.path().to_string_lossy().to_string(); + + // 2. Dump a transfer tx + let tx_args = apply_use_device(vec![ + "transparent-transfer", + "--source", + BERTHA, + "--target", + ALBERT, + "--token", + NAM, + "--amount", + "10.1", + "--gas-price", + "0.00090", + "--signing-keys", + BERTHA_KEY, + "--node", + &validator_one_rpc, + "--dump-tx", + "--output-folder-path", + &output_folder, + ]); + let mut client = run!(test, Bin::Client, tx_args, Some(40))?; + client.assert_success(); + + let offline_tx = find_offline_file(test.test_dir.path(), "tx") + .unwrap() + .expect("Offline tx should be found.") + .to_path_buf() + .display() + .to_string(); + + let bertha_address = find_address(&test, BERTHA).unwrap().to_string(); + let bertha_sk = find_keypair(&test, BERTHA_KEY).unwrap().to_string(); + + // 3. Offline sign a transfer tx + let tx_args = apply_use_device(vec![ + "utils", + "sign-offline", + "--data-path", + &offline_tx, + "--address", + &bertha_address, + "--secret-keys", + &bertha_sk, + "--output-folder-path", + &output_folder, + ]); + let mut client = run!(test, Bin::Client, tx_args, Some(40))?; + client.assert_success(); + + let offline_sig = find_offline_file(test.test_dir.path(), "sig") + .unwrap() + .expect("Offline signature should be found.") + .to_path_buf() + .display() + .to_string(); + + let tx_args = apply_use_device(vec![ + "tx", + "--owner", + BERTHA_KEY, + "--tx-path", + &offline_tx, + "--signatures", + &offline_sig, + "--node", + &validator_one_rpc, + "--gas-payer", + BERTHA_KEY, + ]); + let mut client = run!(test, Bin::Client, tx_args, Some(40))?; + client.assert_success(); + + Ok(()) +} + /// In this test we: /// 1. Run 2 genesis validator ledger nodes and 1 non-validator node /// 2. Cross over epoch to check for consensus with multiple nodes From 6396637c33fc5b29f1c4506b3d0977f382364d59 Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli Date: Wed, 28 Aug 2024 11:35:47 +0200 Subject: [PATCH 7/9] added changelog --- .../improvements/3715-improve-offline-tx-handling.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/unreleased/improvements/3715-improve-offline-tx-handling.md diff --git a/.changelog/unreleased/improvements/3715-improve-offline-tx-handling.md b/.changelog/unreleased/improvements/3715-improve-offline-tx-handling.md new file mode 100644 index 0000000000..9a691e5495 --- /dev/null +++ b/.changelog/unreleased/improvements/3715-improve-offline-tx-handling.md @@ -0,0 +1,3 @@ +- Improve the format of dumped txs. Added command to + generate signature without requiring a network connection. + ([\#3715](https://github.com/anoma/namada/pull/3715)) \ No newline at end of file From aa7aba31635e9e565236a1418c6ac20199aa88df Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli Date: Fri, 30 Aug 2024 09:41:17 +0200 Subject: [PATCH 8/9] convert e2e to integration test --- .github/workflows/scripts/e2e.json | 3 +- crates/apps_lib/src/client/tx.rs | 2 +- crates/tests/src/e2e/helpers.rs | 25 +--- crates/tests/src/e2e/ledger_tests.rs | 103 +-------------- crates/tests/src/integration/helpers.rs | 41 ++++++ crates/tests/src/integration/ledger_tests.rs | 126 ++++++++++++++++++- 6 files changed, 172 insertions(+), 128 deletions(-) diff --git a/.github/workflows/scripts/e2e.json b/.github/workflows/scripts/e2e.json index b8fc16f2d2..3bf6805313 100644 --- a/.github/workflows/scripts/e2e.json +++ b/.github/workflows/scripts/e2e.json @@ -29,6 +29,5 @@ "e2e::wallet_tests::wallet_encrypted_key_cmds": 1, "e2e::wallet_tests::wallet_encrypted_key_cmds_env_var": 1, "e2e::wallet_tests::wallet_unencrypted_key_cmds": 1, - "e2e::ledger_tests::masp_txs_and_queries": 82, - "e2e::ledger_tests::offline_sign": 20 + "e2e::ledger_tests::masp_txs_and_queries": 82 } \ No newline at end of file diff --git a/crates/apps_lib/src/client/tx.rs b/crates/apps_lib/src/client/tx.rs index 2324d76429..fb38a0a53a 100644 --- a/crates/apps_lib/src/client/tx.rs +++ b/crates/apps_lib/src/client/tx.rs @@ -1116,7 +1116,7 @@ where let signature_path = File::create(&output_path) .expect("Should be able to create signature file."); serde_json::to_writer_pretty(signature_path, &signature) - .expect("Signature should be deserializable."); + .expect("Signature should be serializable."); display_line!( namada.io(), diff --git a/crates/tests/src/e2e/helpers.rs b/crates/tests/src/e2e/helpers.rs index 80d77a7cb5..47555d9205 100644 --- a/crates/tests/src/e2e/helpers.rs +++ b/crates/tests/src/e2e/helpers.rs @@ -1,9 +1,9 @@ //! E2E test helpers -use std::fs::{self, File, OpenOptions}; +use std::fs::{File, OpenOptions}; use std::future::Future; use std::io::Write; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::process::Command; use std::str::FromStr; use std::time::{Duration, Instant}; @@ -726,24 +726,3 @@ pub fn get_gaia_gov_address(test: &Test) -> Result { Ok(matched.trim().to_string()) } - -pub fn find_offline_file( - dir: &Path, - extension: &str, -) -> Result> { - // Read the directory entries - for entry in fs::read_dir(dir)? { - let entry = entry?; - let path = entry.path(); - - if path.is_file() { - if let Some(file_extension) = path.extension() { - if file_extension == extension { - return Ok(Some(path)); - } - } - } - } - - Ok(None) -} diff --git a/crates/tests/src/e2e/ledger_tests.rs b/crates/tests/src/e2e/ledger_tests.rs index 923606c878..160338c268 100644 --- a/crates/tests/src/e2e/ledger_tests.rs +++ b/crates/tests/src/e2e/ledger_tests.rs @@ -38,9 +38,8 @@ use setup::constants::*; use setup::Test; use super::helpers::{ - epochs_per_year_from_min_duration, find_keypair, find_offline_file, - get_height, get_pregenesis_wallet, wait_for_block_height, - wait_for_wasm_pre_compile, + epochs_per_year_from_min_duration, get_height, get_pregenesis_wallet, + wait_for_block_height, wait_for_wasm_pre_compile, }; use super::setup::{set_ethereum_bridge_mode, working_dir, NamadaCmd}; use crate::e2e::helpers::{ @@ -124,104 +123,6 @@ fn run_ledger() -> Result<()> { Ok(()) } -#[test] -fn offline_sign() -> Result<()> { - let test = setup::single_node_net()?; - - set_ethereum_bridge_mode( - &test, - &test.net.chain_id, - Who::Validator(0), - ethereum_bridge::ledger::Mode::Off, - None, - ); - - // 1. Run the ledger node - let mut ledger = - start_namada_ledger_node_wait_wasm(&test, Some(0), Some(40))?; - ledger.exp_string("Committed block hash")?; - let _bg_ledger = ledger.background(); - - let validator_one_rpc = get_actor_rpc(&test, Who::Validator(0)); - - let output_folder = test.test_dir.path().to_string_lossy().to_string(); - - // 2. Dump a transfer tx - let tx_args = apply_use_device(vec![ - "transparent-transfer", - "--source", - BERTHA, - "--target", - ALBERT, - "--token", - NAM, - "--amount", - "10.1", - "--gas-price", - "0.00090", - "--signing-keys", - BERTHA_KEY, - "--node", - &validator_one_rpc, - "--dump-tx", - "--output-folder-path", - &output_folder, - ]); - let mut client = run!(test, Bin::Client, tx_args, Some(40))?; - client.assert_success(); - - let offline_tx = find_offline_file(test.test_dir.path(), "tx") - .unwrap() - .expect("Offline tx should be found.") - .to_path_buf() - .display() - .to_string(); - - let bertha_address = find_address(&test, BERTHA).unwrap().to_string(); - let bertha_sk = find_keypair(&test, BERTHA_KEY).unwrap().to_string(); - - // 3. Offline sign a transfer tx - let tx_args = apply_use_device(vec![ - "utils", - "sign-offline", - "--data-path", - &offline_tx, - "--address", - &bertha_address, - "--secret-keys", - &bertha_sk, - "--output-folder-path", - &output_folder, - ]); - let mut client = run!(test, Bin::Client, tx_args, Some(40))?; - client.assert_success(); - - let offline_sig = find_offline_file(test.test_dir.path(), "sig") - .unwrap() - .expect("Offline signature should be found.") - .to_path_buf() - .display() - .to_string(); - - let tx_args = apply_use_device(vec![ - "tx", - "--owner", - BERTHA_KEY, - "--tx-path", - &offline_tx, - "--signatures", - &offline_sig, - "--node", - &validator_one_rpc, - "--gas-payer", - BERTHA_KEY, - ]); - let mut client = run!(test, Bin::Client, tx_args, Some(40))?; - client.assert_success(); - - Ok(()) -} - /// In this test we: /// 1. Run 2 genesis validator ledger nodes and 1 non-validator node /// 2. Cross over epoch to check for consensus with multiple nodes diff --git a/crates/tests/src/integration/helpers.rs b/crates/tests/src/integration/helpers.rs index 3b49ccbaea..487ffab972 100644 --- a/crates/tests/src/integration/helpers.rs +++ b/crates/tests/src/integration/helpers.rs @@ -6,6 +6,7 @@ use namada_core::address::Address; use namada_node::shell::testing::client::run; use namada_node::shell::testing::node::MockNode; use namada_node::shell::testing::utils::{Bin, CapturedOutput}; +use namada_sdk::key::common; /// Query the wallet to get an address from a given alias. pub fn find_address( @@ -37,6 +38,46 @@ pub fn find_address( Ok(address) } +pub fn find_keypair( + node: &MockNode, + alias: impl AsRef, +) -> eyre::Result { + let captured = CapturedOutput::of(|| { + run( + node, + Bin::Wallet, + vec![ + "find", + "--keys", + "--alias", + alias.as_ref(), + "--decrypt", + "--unsafe-show-secret", + ], + ) + }); + assert!(captured.result.is_ok()); + let matched = captured.matches("Public key: .*").unwrap(); + let pk = strip_trailing_newline(matched) + .trim() + .rsplit_once(' ') + .unwrap() + .1; + let matched = captured.matches("Secret key: .*").unwrap(); + let sk = strip_trailing_newline(matched) + .trim() + .rsplit_once(' ') + .unwrap() + .1; + let key = format!("{}{}", sk, pk); + common::SecretKey::from_str(sk).map_err(|e| { + eyre!(format!( + "Key: {} parsed from {}, Error: {}", + key, matched, e + )) + }) +} + fn strip_trailing_newline(input: &str) -> &str { input .strip_suffix("\r\n") diff --git a/crates/tests/src/integration/ledger_tests.rs b/crates/tests/src/integration/ledger_tests.rs index 3d73f4ee73..023ab0dd39 100644 --- a/crates/tests/src/integration/ledger_tests.rs +++ b/crates/tests/src/integration/ledger_tests.rs @@ -1,5 +1,7 @@ use std::collections::BTreeSet; +use std::fs; use std::num::NonZeroU64; +use std::path::{Path, PathBuf}; use std::str::FromStr; use assert_matches::assert_matches; @@ -37,7 +39,7 @@ use crate::e2e::setup::constants::{ }; use crate::e2e::setup::{apply_use_device, ensure_hot_key}; use crate::integration::helpers::{ - find_address, prepare_steward_commission_update_data, + find_address, find_keypair, prepare_steward_commission_update_data, }; use crate::integration::setup; use crate::strings::{ @@ -1671,6 +1673,107 @@ fn change_validator_metadata() -> Result<()> { Ok(()) } +#[test] +fn offline_sign() -> Result<()> { + // This address doesn't matter for tests. But an argument is required. + let validator_one_rpc = "http://127.0.0.1:26567"; + // 1. start the ledger node + let (node, _services) = setup::setup()?; + + let output_folder = tempfile::tempdir().unwrap(); + + // 2. Dump a transfer tx + let captured = CapturedOutput::of(|| { + run( + &node, + Bin::Client, + apply_use_device(vec![ + "transparent-transfer", + "--source", + BERTHA, + "--target", + ALBERT, + "--token", + NAM, + "--amount", + "10.1", + "--gas-price", + "0.00090", + "--signing-keys", + BERTHA_KEY, + "--node", + &validator_one_rpc, + "--dump-tx", + "--output-folder-path", + &output_folder.path().to_str().unwrap(), + ]), + ) + }); + assert!(captured.result.is_ok()); + + let offline_tx = find_file_with_ext(output_folder.path(), "tx") + .unwrap() + .expect("Offline tx should be found.") + .to_path_buf() + .display() + .to_string(); + + let bertha_address = find_address(&node, BERTHA).unwrap().to_string(); + let bertha_sk = find_keypair(&node, BERTHA_KEY).unwrap().to_string(); + + // 2. Dump a transfer tx + let captured = CapturedOutput::of(|| { + run( + &node, + Bin::Client, + apply_use_device(vec![ + "utils", + "sign-offline", + "--data-path", + &offline_tx, + "--address", + &bertha_address, + "--secret-keys", + &bertha_sk, + "--output-folder-path", + &output_folder.path().to_str().unwrap(), + ]), + ) + }); + assert!(captured.result.is_ok()); + + let offline_sig = find_file_with_ext(output_folder.path(), "sig") + .unwrap() + .expect("Offline signature should be found.") + .to_path_buf() + .display() + .to_string(); + + // 3. Offline sign a transfer tx + let captured = CapturedOutput::of(|| { + run( + &node, + Bin::Client, + vec![ + "tx", + "--owner", + BERTHA_KEY, + "--tx-path", + &offline_tx, + "--signatures", + &offline_sig, + "--node", + &validator_one_rpc, + "--gas-payer", + BERTHA_KEY, + ], + ) + }); + assert!(captured.result.is_ok()); + + Ok(()) +} + // Test that fee payment is enforced and aligned with process proposal. The test // generates a tx that subtract funds from the fee payer of a following tx. Test // that wrappers (and fee payments) are evaluated before the inner transactions. @@ -2183,3 +2286,24 @@ fn make_migration_json() -> (Hash, tempfile::NamedTempFile) { std::fs::write(file.path(), json).expect("Test failed"); (hash, file) } + +pub fn find_file_with_ext( + dir: &Path, + extension: &str, +) -> Result> { + // Read the directory entries + for entry in fs::read_dir(dir)? { + let entry = entry?; + let path = entry.path(); + + if path.is_file() { + if let Some(file_extension) = path.extension() { + if file_extension == extension { + return Ok(Some(path)); + } + } + } + } + + Ok(None) +} From 34c6ac4f8912144be260692f15eb704b4bae1dad Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli Date: Fri, 30 Aug 2024 16:17:43 +0200 Subject: [PATCH 9/9] improve wallet support --- crates/apps_lib/src/cli.rs | 47 +++++++++++++++----- crates/apps_lib/src/cli/client.rs | 5 ++- crates/apps_lib/src/client/utils.rs | 1 - crates/tests/src/integration/ledger_tests.rs | 2 +- 4 files changed, 40 insertions(+), 15 deletions(-) diff --git a/crates/apps_lib/src/cli.rs b/crates/apps_lib/src/cli.rs index 9daa31ecf8..40a169963d 100644 --- a/crates/apps_lib/src/cli.rs +++ b/crates/apps_lib/src/cli.rs @@ -33,6 +33,7 @@ const WALLET_CMD: &str = "wallet"; const RELAYER_CMD: &str = "relayer"; pub mod cmds { + use super::args::CliTypes; use super::utils::*; use super::{ args, ArgMatches, CLIENT_CMD, NODE_CMD, RELAYER_CMD, WALLET_CMD, @@ -3195,21 +3196,21 @@ pub mod cmds { } #[derive(Clone, Debug)] - pub struct SignOffline(pub args::SignOffline); + pub struct SignOffline(pub args::SignOffline); impl SubCmd for SignOffline { const CMD: &'static str = "sign-offline"; fn parse(matches: &ArgMatches) -> Option { - matches - .subcommand_matches(Self::CMD) - .map(|matches| Self(args::SignOffline::parse(matches))) + matches.subcommand_matches(Self::CMD).map(|matches| { + Self(args::SignOffline::::parse(matches)) + }) } fn def() -> App { App::new(Self::CMD) .about(wrap!("Offlne sign a transaction.")) - .add_args::() + .add_args::>() } } @@ -3468,7 +3469,7 @@ pub mod args { DefaultFn(|| PortId::from_str("transfer").unwrap()), ); pub const PRE_GENESIS: ArgFlag = flag("pre-genesis"); - pub const PRIVATE_KEYS: ArgMulti = + pub const PRIVATE_KEYS: ArgMulti = arg_multi("secret-keys"); pub const PROPOSAL_PGF_STEWARD: ArgFlag = flag("pgf-stewards"); pub const PROPOSAL_PGF_FUNDING: ArgFlag = flag("pgf-funding"); @@ -8063,18 +8064,18 @@ pub mod args { } #[derive(Clone, Debug)] - pub struct SignOffline { + pub struct SignOffline { pub tx_path: PathBuf, - pub secret_keys: Vec, - pub owner: Address, + pub secret_keys: Vec, + pub owner: C::Address, pub output_folder_path: Option, } - impl Args for SignOffline { + impl Args for SignOffline { fn parse(matches: &ArgMatches) -> Self { let tx_path = DATA_PATH.parse(matches); let secret_keys = PRIVATE_KEYS.parse(matches); - let owner = RAW_ADDRESS.parse(matches); + let owner = OWNER.parse(matches); let output_folder_path = OUTPUT_FOLDER_PATH.parse(matches); Self { @@ -8095,7 +8096,7 @@ pub mod args { "The set of private keys to use to sign the transaction. The \ order matters." ))) - .arg(RAW_ADDRESS.def().help(wrap!("The owner's address."))) + .arg(OWNER.def().help(wrap!("The owner's address."))) .arg( OUTPUT_FOLDER_PATH .def() @@ -8104,6 +8105,28 @@ pub mod args { } } + impl CliToSdk> for SignOffline { + type Error = std::io::Error; + + fn to_sdk( + self, + ctx: &mut Context, + ) -> Result, Self::Error> { + let chain_ctx = ctx.borrow_mut_chain_or_exit(); + + Ok(SignOffline:: { + tx_path: self.tx_path, + secret_keys: self + .secret_keys + .iter() + .map(|key| chain_ctx.get_cached(key)) + .collect(), + owner: chain_ctx.get(&self.owner), + output_folder_path: self.output_folder_path, + }) + } + } + #[derive(Clone, Debug)] pub struct DefaultBaseDir {} diff --git a/crates/apps_lib/src/cli/client.rs b/crates/apps_lib/src/cli/client.rs index 0c26239996..01a50d3860 100644 --- a/crates/apps_lib/src/cli/client.rs +++ b/crates/apps_lib/src/cli/client.rs @@ -817,7 +817,10 @@ impl CliApi { utils::pk_to_tm_address(global_args, args) } ClientUtils::SignOffline(SignOffline(args)) => { - utils::sign_offline(global_args, args).await + let mut ctx = cli::Context::new::(global_args) + .expect("expected to construct a context"); + let args = args.to_sdk(&mut ctx)?; + utils::sign_offline(args).await } ClientUtils::DefaultBaseDir(DefaultBaseDir(args)) => { utils::default_base_dir(global_args, args) diff --git a/crates/apps_lib/src/client/utils.rs b/crates/apps_lib/src/client/utils.rs index 95f54c4ce7..6a227ea362 100644 --- a/crates/apps_lib/src/client/utils.rs +++ b/crates/apps_lib/src/client/utils.rs @@ -1035,7 +1035,6 @@ pub async fn sign_genesis_tx( /// Offline sign a transactions. pub async fn sign_offline( - _global_args: args::Global, args::SignOffline { tx_path, secret_keys, diff --git a/crates/tests/src/integration/ledger_tests.rs b/crates/tests/src/integration/ledger_tests.rs index 023ab0dd39..79393b5e2f 100644 --- a/crates/tests/src/integration/ledger_tests.rs +++ b/crates/tests/src/integration/ledger_tests.rs @@ -1731,7 +1731,7 @@ fn offline_sign() -> Result<()> { "sign-offline", "--data-path", &offline_tx, - "--address", + "--owner", &bertha_address, "--secret-keys", &bertha_sk,