From 134872f20cff3f94f1f4c305ea0e4159f4ded122 Mon Sep 17 00:00:00 2001 From: grumbach Date: Mon, 7 Oct 2024 14:40:30 +0900 Subject: [PATCH 1/4] feat: registers in cli --- autonomi/src/client/data.rs | 2 +- autonomi/src/client/registers.rs | 75 ++++++++--- autonomi/tests/register.rs | 20 +-- autonomi_cli/Cargo.toml | 2 +- autonomi_cli/src/access/data_dir.rs | 19 +++ autonomi_cli/src/access/keys.rs | 103 +++++++++++++++ autonomi_cli/src/access/mod.rs | 11 ++ autonomi_cli/src/access/network.rs | 29 +++++ autonomi_cli/src/commands.rs | 61 ++++++--- autonomi_cli/src/commands/file.rs | 8 +- autonomi_cli/src/commands/register.rs | 119 +++++++++++++++--- autonomi_cli/src/main.rs | 6 +- autonomi_cli/src/utils.rs | 89 ------------- .../reactivate_examples/register_inspect.rs | 2 +- sn_node/tests/verify_data_location.rs | 15 ++- sn_registers/src/address.rs | 3 +- 16 files changed, 400 insertions(+), 164 deletions(-) create mode 100644 autonomi_cli/src/access/data_dir.rs create mode 100644 autonomi_cli/src/access/keys.rs create mode 100644 autonomi_cli/src/access/mod.rs create mode 100644 autonomi_cli/src/access/network.rs delete mode 100644 autonomi_cli/src/utils.rs diff --git a/autonomi/src/client/data.rs b/autonomi/src/client/data.rs index e0650a2ca9..99e1c23e88 100644 --- a/autonomi/src/client/data.rs +++ b/autonomi/src/client/data.rs @@ -236,7 +236,7 @@ impl Client { Ok((proofs, skipped_chunks)) } - async fn get_store_quotes( + pub(crate) async fn get_store_quotes( &self, content_addrs: impl Iterator, ) -> Result, PayError> { diff --git a/autonomi/src/client/registers.rs b/autonomi/src/client/registers.rs index e5e3f24866..a94e198218 100644 --- a/autonomi/src/client/registers.rs +++ b/autonomi/src/client/registers.rs @@ -1,8 +1,19 @@ -use std::collections::BTreeSet; +// Copyright 2024 MaidSafe.net limited. +// +// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. +// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed +// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. Please review the Licences for the specific language governing +// permissions and limitations relating to use of the SAFE Network Software. + +/// Register Secret Key +pub use bls::SecretKey as RegisterSecretKey; +use sn_evm::Amount; +use sn_evm::AttoTokens; +pub use sn_registers::RegisterAddress; use crate::client::data::PayError; use crate::client::Client; -use bls::SecretKey; use bytes::Bytes; use evmlib::wallet::Wallet; use libp2p::kad::{Quorum, Record}; @@ -12,11 +23,11 @@ use sn_networking::PutRecordCfg; use sn_protocol::storage::try_deserialize_record; use sn_protocol::storage::try_serialize_record; use sn_protocol::storage::RecordKind; -use sn_protocol::storage::RegisterAddress; use sn_protocol::NetworkAddress; use sn_registers::Register as ClientRegister; use sn_registers::SignedRegister; use sn_registers::{EntryHash, Permissions}; +use std::collections::BTreeSet; use xor_name::XorName; #[derive(Debug, thiserror::Error)] @@ -63,11 +74,13 @@ impl Register { } impl Client { + /// Generate a new register key + pub fn register_generate_key(&self) -> RegisterSecretKey { + RegisterSecretKey::random() + } + /// Fetches a Register from the network. - pub async fn fetch_register( - &self, - address: RegisterAddress, - ) -> Result { + pub async fn register_get(&self, address: RegisterAddress) -> Result { let network_address = NetworkAddress::from_register_address(address); let key = network_address.to_record_key(); @@ -93,11 +106,11 @@ impl Client { } /// Updates a Register on the network with a new value. This will overwrite existing value(s). - pub async fn update_register( + pub async fn register_update( &self, register: Register, new_value: Bytes, - owner: SecretKey, + owner: RegisterSecretKey, ) -> Result<(), RegisterError> { // Fetch the current register let mut signed_register = register.inner; @@ -112,7 +125,7 @@ impl Client { // Write the new value to all branches let (_, op) = register - .write(new_value.to_vec(), &children, &owner) + .write(new_value.into(), &children, &owner) .map_err(RegisterError::Write)?; // Apply the operation to the register @@ -143,15 +156,49 @@ impl Client { Ok(()) } - /// Creates a new Register with an initial value and uploads it to the network. - pub async fn create_register( + /// Get the cost to create a register + pub async fn register_cost( + &self, + name: String, + owner: RegisterSecretKey, + ) -> Result { + // get register address + let pk = owner.public_key(); + let name = XorName::from_content_parts(&[name.as_bytes()]); + let permissions = Permissions::new_with([pk]); + let register = ClientRegister::new(pk, name, permissions); + let reg_xor = register.address().xorname(); + + // get cost to store register + // NB TODO: register should be priced differently from other data + let cost_map = self.get_store_quotes(std::iter::once(reg_xor)).await?; + let total_cost = AttoTokens::from_atto( + cost_map + .values() + .map(|quote| quote.2.cost.as_atto()) + .sum::(), + ); + + Ok(total_cost) + } + + /// Get the address of a register from its name and owner + pub fn register_address(&self, name: &str, owner: &RegisterSecretKey) -> RegisterAddress { + let pk = owner.public_key(); + let name = XorName::from_content_parts(&[name.as_bytes()]); + RegisterAddress::new(name, pk) + } + + /// Creates a new Register with a name and an initial value and uploads it to the network. + pub async fn register_create( &self, value: Bytes, - name: XorName, - owner: SecretKey, + name: &str, + owner: RegisterSecretKey, wallet: &Wallet, ) -> Result { let pk = owner.public_key(); + let name = XorName::from_content_parts(&[name.as_bytes()]); // Owner can write to the register. let permissions = Permissions::new_with([pk]); diff --git a/autonomi/tests/register.rs b/autonomi/tests/register.rs index 3cee58d0d2..f03cf34a4c 100644 --- a/autonomi/tests/register.rs +++ b/autonomi/tests/register.rs @@ -4,10 +4,10 @@ mod common; use autonomi::Client; use bytes::Bytes; +use rand::Rng; use std::time::Duration; use test_utils::evm::get_funded_wallet; use tokio::time::sleep; -use xor_name::XorName; #[tokio::test] async fn register() { @@ -20,30 +20,30 @@ async fn register() { let key = bls::SecretKey::random(); // Create a register with the value [1, 2, 3, 4] + let rand_name: String = rand::thread_rng() + .sample_iter(&rand::distributions::Alphanumeric) + .take(10) + .map(char::from) + .collect(); let register = client - .create_register( - vec![1, 2, 3, 4].into(), - XorName::random(&mut rand::thread_rng()), - key.clone(), - &wallet, - ) + .register_create(vec![1, 2, 3, 4].into(), &rand_name, key.clone(), &wallet) .await .unwrap(); sleep(Duration::from_secs(10)).await; // Fetch the register again - let register = client.fetch_register(*register.address()).await.unwrap(); + let register = client.register_get(*register.address()).await.unwrap(); // Update the register with the value [5, 6, 7, 8] client - .update_register(register.clone(), vec![5, 6, 7, 8].into(), key) + .register_update(register.clone(), vec![5, 6, 7, 8].into(), key) .await .unwrap(); sleep(Duration::from_secs(2)).await; // Fetch and verify the register contains the updated value - let register = client.fetch_register(*register.address()).await.unwrap(); + let register = client.register_get(*register.address()).await.unwrap(); assert_eq!(register.values(), vec![Bytes::from(vec![5, 6, 7, 8])]); } diff --git a/autonomi_cli/Cargo.toml b/autonomi_cli/Cargo.toml index e779493126..b6a0678ee5 100644 --- a/autonomi_cli/Cargo.toml +++ b/autonomi_cli/Cargo.toml @@ -10,7 +10,7 @@ metrics = ["sn_logging/process-metrics"] network-contacts = ["sn_peers_acquisition/network-contacts"] [dependencies] -autonomi = { path = "../autonomi", version = "0.1.0", features = ["data", "files"] } +autonomi = { path = "../autonomi", version = "0.1.0", features = ["data", "files", "registers"] } clap = { version = "4.2.1", features = ["derive"] } color-eyre = "~0.6" dirs-next = "~2.0.0" diff --git a/autonomi_cli/src/access/data_dir.rs b/autonomi_cli/src/access/data_dir.rs new file mode 100644 index 0000000000..af0db16c2c --- /dev/null +++ b/autonomi_cli/src/access/data_dir.rs @@ -0,0 +1,19 @@ +// Copyright 2024 MaidSafe.net limited. +// +// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. +// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed +// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. Please review the Licences for the specific language governing +// permissions and limitations relating to use of the SAFE Network Software. + +use color_eyre::eyre::{eyre, Context, Result}; +use std::path::PathBuf; + +pub fn get_client_data_dir_path() -> Result { + let mut home_dirs = dirs_next::data_dir() + .ok_or_else(|| eyre!("Failed to obtain data dir, your OS might not be supported."))?; + home_dirs.push("safe"); + home_dirs.push("client"); + std::fs::create_dir_all(home_dirs.as_path()).wrap_err("Failed to create data dir")?; + Ok(home_dirs) +} diff --git a/autonomi_cli/src/access/keys.rs b/autonomi_cli/src/access/keys.rs new file mode 100644 index 0000000000..a11de06b7a --- /dev/null +++ b/autonomi_cli/src/access/keys.rs @@ -0,0 +1,103 @@ +// Copyright 2024 MaidSafe.net limited. +// +// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. +// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed +// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. Please review the Licences for the specific language governing +// permissions and limitations relating to use of the SAFE Network Software. + +use autonomi::client::registers::RegisterSecretKey; +use autonomi::Wallet; +use color_eyre::eyre::{Context, Result}; +use color_eyre::Section; +use std::env; +use std::fs; +use std::path::PathBuf; + +const SECRET_KEY_ENV: &str = "SECRET_KEY"; +const REGISTER_SIGNING_KEY_ENV: &str = "REGISTER_SIGNING_KEY"; + +const SECRET_KEY_FILE: &str = "secret_key"; +const REGISTER_SIGNING_KEY_FILE: &str = "register_signing_key"; + +/// EVM wallet +pub fn load_evm_wallet() -> Result { + let secret_key = + get_secret_key().wrap_err("The secret key is required to perform this action")?; + let network = crate::network::get_evm_network_from_environment() + .wrap_err("Failed to load EVM network")?; + let wallet = Wallet::new_from_private_key(network, &secret_key) + .wrap_err("Failed to load EVM wallet from key")?; + Ok(wallet) +} + +/// EVM wallet private key +pub fn get_secret_key() -> Result { + // try env var first + let why_env_failed = match env::var(SECRET_KEY_ENV) { + Ok(key) => return Ok(key), + Err(e) => e, + }; + + // try from data dir + let dir = super::data_dir::get_client_data_dir_path() + .wrap_err(format!("Failed to obtain secret key from env var: {why_env_failed}, reading from disk also failed as couldn't access data dir")) + .with_suggestion(|| format!("make sure you've provided the {SECRET_KEY_ENV} env var"))?; + + // load the key from file + let key_path = dir.join(SECRET_KEY_FILE); + fs::read_to_string(&key_path) + .wrap_err("Failed to read secret key from file") + .with_suggestion(|| format!("make sure you've provided the {SECRET_KEY_ENV} env var or have the key in a file at {key_path:?}")) + .with_suggestion(|| "the secret key should be a hex encoded string of your evm wallet private key") +} + +pub fn create_register_signing_key_file(key: RegisterSecretKey) -> Result { + let dir = super::data_dir::get_client_data_dir_path() + .wrap_err("Could not access directory to write key to")?; + let file_path = dir.join(REGISTER_SIGNING_KEY_FILE); + fs::write(&file_path, key.to_hex()).wrap_err("Could not write key to file")?; + Ok(file_path) +} + +fn parse_register_signing_key(key_hex: &str) -> Result { + RegisterSecretKey::from_hex(key_hex) + .wrap_err("Failed to parse register signing key") + .with_suggestion(|| { + "the register signing key should be a hex encoded string of a bls secret key" + }) + .with_suggestion(|| { + "you can generate a new secret key with the `register generate-key` subcommand" + }) +} + +pub fn get_register_signing_key() -> Result { + // try env var first + let why_env_failed = match env::var(REGISTER_SIGNING_KEY_ENV) { + Ok(key) => return parse_register_signing_key(&key), + Err(e) => e, + }; + + // try from data dir + let dir = super::data_dir::get_client_data_dir_path() + .wrap_err(format!("Failed to obtain register signing key from env var: {why_env_failed}, reading from disk also failed as couldn't access data dir")) + .with_suggestion(|| format!("make sure you've provided the {REGISTER_SIGNING_KEY_ENV} env var")) + .with_suggestion(|| "you can generate a new secret key with the `register generate-key` subcommand")?; + + // load the key from file + let key_path = dir.join(REGISTER_SIGNING_KEY_FILE); + let key_hex = fs::read_to_string(&key_path) + .wrap_err("Failed to read secret key from file") + .with_suggestion(|| format!("make sure you've provided the {REGISTER_SIGNING_KEY_ENV} env var or have the key in a file at {key_path:?}")) + .with_suggestion(|| "you can generate a new secret key with the `register generate-key` subcommand")?; + + // parse the key + parse_register_signing_key(&key_hex) +} + +pub fn get_register_signing_key_path() -> Result { + let dir = super::data_dir::get_client_data_dir_path() + .wrap_err("Could not access directory for register signing key")?; + let file_path = dir.join(REGISTER_SIGNING_KEY_FILE); + Ok(file_path) +} diff --git a/autonomi_cli/src/access/mod.rs b/autonomi_cli/src/access/mod.rs new file mode 100644 index 0000000000..ac80eeca88 --- /dev/null +++ b/autonomi_cli/src/access/mod.rs @@ -0,0 +1,11 @@ +// Copyright 2024 MaidSafe.net limited. +// +// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. +// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed +// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. Please review the Licences for the specific language governing +// permissions and limitations relating to use of the SAFE Network Software. + +pub mod data_dir; +pub mod keys; +pub mod network; diff --git a/autonomi_cli/src/access/network.rs b/autonomi_cli/src/access/network.rs new file mode 100644 index 0000000000..b611161bcd --- /dev/null +++ b/autonomi_cli/src/access/network.rs @@ -0,0 +1,29 @@ +// Copyright 2024 MaidSafe.net limited. +// +// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. +// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed +// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. Please review the Licences for the specific language governing +// permissions and limitations relating to use of the SAFE Network Software. + +use autonomi::Multiaddr; +use autonomi::Network; +use color_eyre::eyre::eyre; +use color_eyre::eyre::Context; +use color_eyre::Result; +use color_eyre::Section; +use sn_peers_acquisition::PeersArgs; + +use sn_peers_acquisition::SAFE_PEERS_ENV; + +pub async fn get_peers(peers: PeersArgs) -> Result> { + peers.get_peers().await + .wrap_err("Please provide valid Network peers to connect to") + .with_suggestion(|| format!("make sure you've provided network peers using the --peers option or the {SAFE_PEERS_ENV} env var")) + .with_suggestion(|| "a peer address looks like this: /ip4/42.42.42.42/udp/4242/quic-v1/p2p/B64nodePeerIDvdjb3FAJF4ks3moreBase64CharsHere") +} + +pub(crate) fn get_evm_network_from_environment() -> Result { + evmlib::utils::evm_network_from_env() + .map_err(|err| eyre!("Failed to get EVM network from environment: {err}")) +} diff --git a/autonomi_cli/src/commands.rs b/autonomi_cli/src/commands.rs index 12d4af26f1..a3bd5064a9 100644 --- a/autonomi_cli/src/commands.rs +++ b/autonomi_cli/src/commands.rs @@ -64,6 +64,14 @@ pub enum FileCmd { #[derive(Subcommand, Debug)] pub enum RegisterCmd { + /// Generate a new register key. + GenerateKey { + /// Overwrite existing key if it exists + /// Warning: overwriting the existing key will result in loss of access to any existing registers created using that key + #[arg(short, long)] + overwrite: bool, + }, + /// Estimate cost to register a name. Cost { /// The name to register. @@ -80,16 +88,26 @@ pub enum RegisterCmd { /// Edit an existing register. Edit { - /// The name of the register. - name: String, + /// Use the name of the register instead of the address + /// Note that only the owner of the register can use this shorthand as the address can be generated from the name and register key. + #[arg(short, long)] + name: bool, + /// The address of the register + /// With the name option on the address will be used as a name + address: String, /// The new value to store in the register. value: String, }, /// Get the value of a register. Get { - /// The name of the register. - name: String, + /// Use the name of the register instead of the address + /// Note that only the owner of the register can use this shorthand as the address can be generated from the name and register key. + #[arg(short, long)] + name: bool, + /// The address of the register + /// With the name option on the address will be used as a name + address: String, }, /// List previous registers @@ -109,27 +127,36 @@ pub enum VaultCmd { } pub async fn handle_subcommand(opt: Opt) -> Result<()> { - let peers = crate::utils::get_peers(opt.peers).await?; + let peers = crate::access::network::get_peers(opt.peers); let cmd = opt.command; match cmd { SubCmd::File { command } => match command { - FileCmd::Cost { file } => file::cost(&file, peers).await, - FileCmd::Upload { file } => file::upload(&file, peers).await, - FileCmd::Download { addr, dest_file } => file::download(&addr, &dest_file, peers).await, - FileCmd::List => file::list(peers), + FileCmd::Cost { file } => file::cost(&file, peers.await?).await, + FileCmd::Upload { file } => file::upload(&file, peers.await?).await, + FileCmd::Download { addr, dest_file } => { + file::download(&addr, &dest_file, peers.await?).await + } + FileCmd::List => file::list(peers.await?), }, SubCmd::Register { command } => match command { - RegisterCmd::Cost { name } => register::cost(&name, peers), - RegisterCmd::Create { name, value } => register::create(&name, &value, peers), - RegisterCmd::Edit { name, value } => register::edit(&name, &value, peers), - RegisterCmd::Get { name } => register::get(&name, peers), - RegisterCmd::List => register::list(peers), + RegisterCmd::GenerateKey { overwrite } => register::generate_key(overwrite), + RegisterCmd::Cost { name } => register::cost(&name, peers.await?).await, + RegisterCmd::Create { name, value } => { + register::create(&name, &value, peers.await?).await + } + RegisterCmd::Edit { + address, + name, + value, + } => register::edit(address, name, &value, peers.await?).await, + RegisterCmd::Get { address, name } => register::get(address, name, peers.await?).await, + RegisterCmd::List => register::list(peers.await?), }, SubCmd::Vault { command } => match command { - VaultCmd::Cost => vault::cost(peers), - VaultCmd::Create => vault::create(peers), - VaultCmd::Sync => vault::sync(peers), + VaultCmd::Cost => vault::cost(peers.await?), + VaultCmd::Create => vault::create(peers.await?), + VaultCmd::Sync => vault::sync(peers.await?), }, } } diff --git a/autonomi_cli/src/commands/file.rs b/autonomi_cli/src/commands/file.rs index 45b60a24df..672c779f89 100644 --- a/autonomi_cli/src/commands/file.rs +++ b/autonomi_cli/src/commands/file.rs @@ -8,7 +8,6 @@ use autonomi::client::address::xorname_to_str; use autonomi::Multiaddr; -use autonomi::Wallet; use color_eyre::eyre::Context; use color_eyre::eyre::Result; use std::path::PathBuf; @@ -28,12 +27,7 @@ pub async fn cost(file: &str, peers: Vec) -> Result<()> { } pub async fn upload(file: &str, peers: Vec) -> Result<()> { - let secret_key = crate::utils::get_secret_key() - .wrap_err("The secret key is required to perform this action")?; - let network = crate::utils::get_evm_network_from_environment()?; - let wallet = - Wallet::new_from_private_key(network, &secret_key).wrap_err("Failed to load wallet")?; - + let wallet = crate::keys::load_evm_wallet()?; let mut client = crate::actions::connect_to_network(peers).await?; println!("Uploading data to network..."); diff --git a/autonomi_cli/src/commands/register.rs b/autonomi_cli/src/commands/register.rs index 6afa26c755..3f73f6d650 100644 --- a/autonomi_cli/src/commands/register.rs +++ b/autonomi_cli/src/commands/register.rs @@ -6,37 +6,128 @@ // KIND, either express or implied. Please review the Licences for the specific language governing // permissions and limitations relating to use of the SAFE Network Software. +use autonomi::client::registers::RegisterAddress; +use autonomi::client::registers::RegisterSecretKey; use autonomi::Multiaddr; +use color_eyre::eyre::eyre; use color_eyre::eyre::Context; use color_eyre::eyre::Result; +use color_eyre::Section; -pub fn cost(_name: &str, _peers: Vec) -> Result<()> { - let _register_key = crate::utils::get_register_signing_key() +pub fn generate_key(overwrite: bool) -> Result<()> { + // check if the key already exists + let key_path = crate::keys::get_register_signing_key_path()?; + if key_path.exists() && !overwrite { + return Err(eyre!("Register key already exists at: {}", key_path.display())) + .with_suggestion(|| "if you want to overwrite the existing key, run the command with the --overwrite flag") + .with_warning(|| "overwriting the existing key might result in loss of access to any existing registers created using that key"); + } + + // generate and write a new key to file + let key = RegisterSecretKey::random(); + let path = crate::keys::create_register_signing_key_file(key) + .wrap_err("Failed to create new register key")?; + println!("✅ Created new register key at: {}", path.display()); + Ok(()) +} + +pub async fn cost(name: &str, peers: Vec) -> Result<()> { + let register_key = crate::keys::get_register_signing_key() .wrap_err("The register key is required to perform this action")?; - println!("The register feature is coming soon!"); + let client = crate::actions::connect_to_network(peers).await?; + + let cost = client + .register_cost(name.to_string(), register_key) + .await + .wrap_err("Failed to get cost for register")?; + println!("✅ The estimated cost to create a register with name {name} is: {cost}"); Ok(()) } -pub fn create(_name: &str, _value: &str, _peers: Vec) -> Result<()> { - let _secret_key = crate::utils::get_secret_key() - .wrap_err("The secret key is required to perform this action")?; - let _register_key = crate::utils::get_register_signing_key() +pub async fn create(name: &str, value: &str, peers: Vec) -> Result<()> { + let wallet = crate::keys::load_evm_wallet()?; + let register_key = crate::keys::get_register_signing_key() .wrap_err("The register key is required to perform this action")?; - println!("The register feature is coming soon!"); + let client = crate::actions::connect_to_network(peers).await?; + + println!("Creating register with name: {name}"); + let register = client + .register_create( + value.as_bytes().to_vec().into(), + name, + register_key, + &wallet, + ) + .await + .wrap_err("Failed to create register")?; + let address = register.address(); + + println!("✅ Register created at address: {address}"); + println!("With name: {name}"); + println!("And initial value: [{value}]"); Ok(()) } -pub fn edit(_name: &str, _value: &str, _peers: Vec) -> Result<()> { - let _register_key = crate::utils::get_register_signing_key() +pub async fn edit(address: String, name: bool, value: &str, peers: Vec) -> Result<()> { + let register_key = crate::keys::get_register_signing_key() .wrap_err("The register key is required to perform this action")?; - println!("The register feature is coming soon!"); + let client = crate::actions::connect_to_network(peers).await?; + + let address = if name { + client.register_address(&address, ®ister_key) + } else { + RegisterAddress::from_hex(&address) + .wrap_err(format!("Failed to parse register address: {address}"))? + }; + + println!("Getting register at address: {address}"); + let register = client + .register_get(address) + .await + .wrap_err(format!("Failed to get register at address: {address}"))?; + println!("Found register at address: {address}"); + + println!("Updating register with new value: {value}"); + client + .register_update(register, value.as_bytes().to_vec().into(), register_key) + .await + .wrap_err(format!("Failed to update register at address: {address}"))?; + + println!("✅ Successfully updated register"); + println!("With value: [{value}]"); + Ok(()) } -pub fn get(_name: &str, _peers: Vec) -> Result<()> { - let _register_key = crate::utils::get_register_signing_key() +pub async fn get(address: String, name: bool, peers: Vec) -> Result<()> { + let register_key = crate::keys::get_register_signing_key() .wrap_err("The register key is required to perform this action")?; - println!("The register feature is coming soon!"); + let client = crate::actions::connect_to_network(peers).await?; + + let address = if name { + client.register_address(&address, ®ister_key) + } else { + RegisterAddress::from_hex(&address) + .wrap_err(format!("Failed to parse register address: {address}"))? + }; + + println!("Getting register at address: {address}"); + let register = client + .register_get(address) + .await + .wrap_err(format!("Failed to get register at address: {address}"))?; + let values = register.values(); + + println!("✅ Register found at address: {address}"); + match values.as_slice() { + [one] => println!("With value: [{:?}]", String::from_utf8_lossy(one)), + _ => { + println!("With multiple concurrent values:"); + for value in values.iter() { + println!("[{:?}]", String::from_utf8_lossy(value)); + } + } + } Ok(()) } diff --git a/autonomi_cli/src/main.rs b/autonomi_cli/src/main.rs index 6aaa446582..8e7a9e1a5b 100644 --- a/autonomi_cli/src/main.rs +++ b/autonomi_cli/src/main.rs @@ -9,11 +9,15 @@ #[macro_use] extern crate tracing; +mod access; mod actions; mod commands; mod log_metrics; mod opt; -mod utils; + +pub use access::data_dir; +pub use access::keys; +pub use access::network; use clap::Parser; use color_eyre::Result; diff --git a/autonomi_cli/src/utils.rs b/autonomi_cli/src/utils.rs deleted file mode 100644 index 2d7cce6d19..0000000000 --- a/autonomi_cli/src/utils.rs +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright 2024 MaidSafe.net limited. -// -// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. -// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed -// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. Please review the Licences for the specific language governing -// permissions and limitations relating to use of the SAFE Network Software. - -use autonomi::Multiaddr; -use autonomi::Network; -use color_eyre::eyre::eyre; -use color_eyre::eyre::Context; -use color_eyre::Result; -use color_eyre::Section; -use sn_peers_acquisition::PeersArgs; -use std::env; -use std::fs; -use std::path::PathBuf; - -use sn_peers_acquisition::SAFE_PEERS_ENV; - -// NB TODO: use those as return values for the functions below -// use autonomi::register::RegisterKey; -// use autonomi::wallet::WalletKey; - -const SECRET_KEY: &str = "SECRET_KEY"; -const REGISTER_SIGNING_KEY: &str = "REGISTER_SIGNING_KEY"; - -const SECRET_KEY_FILE: &str = "secret_key"; -const REGISTER_SIGNING_KEY_FILE: &str = "register_signing_key"; - -pub fn get_secret_key() -> Result { - // try env var first - let why_env_failed = match env::var(SECRET_KEY) { - Ok(key) => return Ok(key), - Err(e) => e, - }; - - // try from data dir - let dir = get_client_data_dir_path() - .wrap_err(format!("Failed to obtain secret key from env var: {why_env_failed}, reading from disk also failed as couldn't access data dir")) - .with_suggestion(|| format!("make sure you've provided the {SECRET_KEY} env var"))?; - - // load the key from file - let key_path = dir.join(SECRET_KEY_FILE); - fs::read_to_string(&key_path) - .wrap_err("Failed to read secret key from file") - .with_suggestion(|| format!("make sure you've provided the {SECRET_KEY} env var or have the key in a file at {key_path:?}")) -} - -pub fn get_register_signing_key() -> Result { - // try env var first - let why_env_failed = match env::var(REGISTER_SIGNING_KEY) { - Ok(key) => return Ok(key), - Err(e) => e, - }; - - // try from data dir - let dir = get_client_data_dir_path() - .wrap_err(format!("Failed to obtain register signing key from env var: {why_env_failed}, reading from disk also failed as couldn't access data dir")) - .with_suggestion(|| format!("make sure you've provided the {REGISTER_SIGNING_KEY} env var"))?; - - // load the key from file - let key_path = dir.join(REGISTER_SIGNING_KEY_FILE); - fs::read_to_string(&key_path) - .wrap_err("Failed to read secret key from file") - .with_suggestion(|| format!("make sure you've provided the {REGISTER_SIGNING_KEY} env var or have the key in a file at {key_path:?}")) -} - -pub fn get_client_data_dir_path() -> Result { - let mut home_dirs = dirs_next::data_dir() - .ok_or_else(|| eyre!("Failed to obtain data dir, your OS might not be supported."))?; - home_dirs.push("safe"); - home_dirs.push("client"); - std::fs::create_dir_all(home_dirs.as_path()).wrap_err("Failed to create data dir")?; - Ok(home_dirs) -} - -pub async fn get_peers(peers: PeersArgs) -> Result> { - peers.get_peers().await - .wrap_err("Please provide valid Network peers to connect to") - .with_suggestion(|| format!("make sure you've provided network peers using the --peers option or the {SAFE_PEERS_ENV} env var")) - .with_suggestion(|| "a peer address looks like this: /ip4/42.42.42.42/udp/4242/quic-v1/p2p/B64nodePeerIDvdjb3FAJF4ks3moreBase64CharsHere") -} - -pub(crate) fn get_evm_network_from_environment() -> Result { - evmlib::utils::evm_network_from_env() - .map_err(|err| eyre!("Failed to get EVM network from environment: {err}")) -} diff --git a/sn_node/reactivate_examples/register_inspect.rs b/sn_node/reactivate_examples/register_inspect.rs index 2873aa1139..03f35ffa6e 100644 --- a/sn_node/reactivate_examples/register_inspect.rs +++ b/sn_node/reactivate_examples/register_inspect.rs @@ -73,7 +73,7 @@ // .join("client"); // let wallet = load_account_wallet_or_create_with_mnemonic(&root_dir, None) -// .wrap_err("Unable to read wallet file in {root_dir:?}") +// .wrap_err(format!"Unable to read wallet file in {root_dir:?}")) // .suggestion( // "If you have an old wallet file, it may no longer be compatible. Try removing it", // )?; diff --git a/sn_node/tests/verify_data_location.rs b/sn_node/tests/verify_data_location.rs index 3a1c091dc1..d387bd76b6 100644 --- a/sn_node/tests/verify_data_location.rs +++ b/sn_node/tests/verify_data_location.rs @@ -34,7 +34,6 @@ use std::{ }; use tonic::Request; use tracing::{debug, error, info}; -use xor_name::XorName; const CHUNK_SIZE: usize = 1024; @@ -374,13 +373,13 @@ async fn store_registers( let key = bls::SecretKey::random(); // Create a register with the value [1, 2, 3, 4] + let rand_name: String = rand::thread_rng() + .sample_iter(&rand::distributions::Alphanumeric) + .take(10) + .map(char::from) + .collect(); let register = client - .create_register( - vec![1, 2, 3, 4].into(), - XorName::random(&mut rand::thread_rng()), - key.clone(), - wallet, - ) + .register_create(vec![1, 2, 3, 4].into(), &rand_name, key.clone(), wallet) .await?; println!("Created Register at {:?}", register.address()); @@ -389,7 +388,7 @@ async fn store_registers( // Update the register with the value [5, 6, 7, 8] client - .update_register(register.clone(), vec![5, 6, 7, 8].into(), key) + .register_update(register.clone(), vec![5, 6, 7, 8].into(), key) .await?; println!("Updated Register at {:?}", register.address()); diff --git a/sn_registers/src/address.rs b/sn_registers/src/address.rs index d0cdacb0ba..f8f2c346a1 100644 --- a/sn_registers/src/address.rs +++ b/sn_registers/src/address.rs @@ -26,8 +26,9 @@ pub struct RegisterAddress { } impl Display for RegisterAddress { + /// Display the register address in hex format that can be parsed by `RegisterAddress::from_hex`. fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}({:?})", &self.to_hex()[0..6], self.xorname()) + write!(f, "{}", &self.to_hex()) } } From ca2bc0d52d56be89f6ba139114b237bb53b878a6 Mon Sep 17 00:00:00 2001 From: grumbach Date: Mon, 7 Oct 2024 14:43:18 +0900 Subject: [PATCH 2/4] chore: improve err msgs --- autonomi_cli/src/commands/register.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/autonomi_cli/src/commands/register.rs b/autonomi_cli/src/commands/register.rs index 3f73f6d650..7971f145da 100644 --- a/autonomi_cli/src/commands/register.rs +++ b/autonomi_cli/src/commands/register.rs @@ -77,7 +77,8 @@ pub async fn edit(address: String, name: bool, value: &str, peers: Vec) -> Result<( client.register_address(&address, ®ister_key) } else { RegisterAddress::from_hex(&address) - .wrap_err(format!("Failed to parse register address: {address}"))? + .wrap_err(format!("Failed to parse register address: {address}")) + .with_suggestion(|| "if you want to use the name as the address, run the command with the --name flag")? }; println!("Getting register at address: {address}"); From c7994e013244f82925ed2c3c93bb3f3d7d16484a Mon Sep 17 00:00:00 2001 From: grumbach Date: Mon, 7 Oct 2024 19:03:46 +0900 Subject: [PATCH 3/4] feat: wasm docs --- autonomi/WASM_docs.md | 170 +++++++++++++++++++++++++++++ autonomi/src/client/data.rs | 4 +- autonomi/src/lib.rs | 2 +- autonomi_cli/src/access/network.rs | 4 +- 4 files changed, 176 insertions(+), 4 deletions(-) create mode 100644 autonomi/WASM_docs.md diff --git a/autonomi/WASM_docs.md b/autonomi/WASM_docs.md new file mode 100644 index 0000000000..995809b8bd --- /dev/null +++ b/autonomi/WASM_docs.md @@ -0,0 +1,170 @@ +## JavaScript Autonomi API Documentation + +Note that this is a first version and will be subject to change. + +### **Client** + +The `Client` object allows interaction with the network to store and retrieve data. Below are the available methods for the `Client` class. + +#### **Constructor** + +```javascript +let client = await new Client([multiaddress]); +``` + +- **multiaddress** (Array of Strings): A list of network addresses for the client to connect to. + +Example: +```javascript +let client = await new Client(["/ip4/127.0.0.1/tcp/36075/ws/p2p/12D3KooWALb...BhDAfJY"]); +``` + +#### **Methods** + +##### **put(data, wallet)** + +Uploads a piece of encrypted data to the network. + +```javascript +let result = await client.put(data, wallet); +``` + +- **data** (Uint8Array): The data to be stored. +- **wallet** (Wallet): The wallet used to pay for the storage. + +Returns: +- **result** (XorName): The XOR address of the stored data. + +Example: +```javascript +let wallet = getFundedWallet(); +let data = new Uint8Array([1, 2, 3]); +let result = await client.put(data, wallet); +``` + +##### **get(data_map_addr)** + +Fetches encrypted data from the network using its XOR address. + +```javascript +let data = await client.get(data_map_addr); +``` + +- **data_map_addr** (XorName): The XOR address of the data to fetch. + +Returns: +- **data** (Uint8Array): The fetched data. + +Example: +```javascript +let data = await client.get(result); +``` + +##### **cost(data)** + +Gets the cost of storing the provided data on the network. + +```javascript +let cost = await client.cost(data); +``` + +- **data** (Uint8Array): The data whose storage cost you want to calculate. + +Returns: +- **cost** (AttoTokens): The calculated cost for storing the data. + +Example: +```javascript +let cost = await client.cost(new Uint8Array([1, 2, 3])); +``` + +--- + +### **Wallet** + +The `Wallet` object represents an Ethereum wallet used for data payments. + +#### **Methods** + +##### **new_from_private_key(network, private_key)** + +Creates a new wallet using the given private key. + +```javascript +let wallet = Wallet.new_from_private_key(network, private_key); +``` + +- **network** (EvmNetwork): The network to which the wallet connects. +- **private_key** (String): The private key of the wallet. + +Returns: +- **wallet** (Wallet): The created wallet. + +Example: +```javascript +let wallet = Wallet.new_from_private_key(EvmNetwork.default(), "your_private_key_here"); +``` + +##### **address()** + +Gets the wallet’s address. + +```javascript +let address = wallet.address(); +``` + +Returns: +- **address** (Address): The wallet's address. + +Example: +```javascript +let wallet = Wallet.new_from_private_key(EvmNetwork.default(), "your_private_key_here"); +let address = wallet.address(); +``` + +--- + +### **EvmNetwork** + +The `EvmNetwork` object represents the blockchain network. + +#### **Methods** + +##### **default()** + +Connects to the default network. + +```javascript +let network = EvmNetwork.default(); +``` + +Returns: +- **network** (EvmNetwork): The default network. + +Example: +```javascript +let network = EvmNetwork.default(); +``` + +--- + +### Example Usage: + +```javascript +let client = await new Client(["/ip4/127.0.0.1/tcp/36075/ws/p2p/12D3KooWALb...BhDAfJY"]); +console.log("connected"); + +let wallet = Wallet.new_from_private_key(EvmNetwork.default(), "your_private_key_here"); +console.log("wallet retrieved"); + +let data = new Uint8Array([1, 2, 3]); +let result = await client.put(data, wallet); +console.log("Data stored at:", result); + +let fetchedData = await client.get(result); +console.log("Data retrieved:", fetchedData); +``` + +--- + +This documentation covers the basic usage of `Client`, `Wallet`, and `EvmNetwork` types in the JavaScript API. \ No newline at end of file diff --git a/autonomi/src/client/data.rs b/autonomi/src/client/data.rs index 99e1c23e88..cdfa7aac25 100644 --- a/autonomi/src/client/data.rs +++ b/autonomi/src/client/data.rs @@ -186,7 +186,8 @@ impl Client { Ok(map_xor_name) } - pub(crate) async fn cost(&self, data: Bytes) -> Result { + /// Get the cost of storing a piece of data. + pub async fn cost(&self, data: Bytes) -> Result { let now = std::time::Instant::now(); let (data_map_chunk, chunks) = encrypt(data)?; @@ -209,6 +210,7 @@ impl Client { Ok(total_cost) } + /// Pay for the chunks and get the proof of payment. pub(crate) async fn pay( &self, content_addrs: impl Iterator, diff --git a/autonomi/src/lib.rs b/autonomi/src/lib.rs index 0e8ff3f61d..55f2415786 100644 --- a/autonomi/src/lib.rs +++ b/autonomi/src/lib.rs @@ -25,7 +25,7 @@ pub mod client; #[cfg(feature = "data")] mod self_encryption; -pub use sn_evm::EvmNetwork as Network; +pub use sn_evm::EvmNetwork; pub use sn_evm::EvmWallet as Wallet; #[doc(no_inline)] // Place this under 'Re-exports' in the docs. diff --git a/autonomi_cli/src/access/network.rs b/autonomi_cli/src/access/network.rs index b611161bcd..bfc77e851f 100644 --- a/autonomi_cli/src/access/network.rs +++ b/autonomi_cli/src/access/network.rs @@ -7,7 +7,7 @@ // permissions and limitations relating to use of the SAFE Network Software. use autonomi::Multiaddr; -use autonomi::Network; +use autonomi::EvmNetwork; use color_eyre::eyre::eyre; use color_eyre::eyre::Context; use color_eyre::Result; @@ -23,7 +23,7 @@ pub async fn get_peers(peers: PeersArgs) -> Result> { .with_suggestion(|| "a peer address looks like this: /ip4/42.42.42.42/udp/4242/quic-v1/p2p/B64nodePeerIDvdjb3FAJF4ks3moreBase64CharsHere") } -pub(crate) fn get_evm_network_from_environment() -> Result { +pub(crate) fn get_evm_network_from_environment() -> Result { evmlib::utils::evm_network_from_env() .map_err(|err| eyre!("Failed to get EVM network from environment: {err}")) } From 9ec8990b2dcc6758a58ea3922f572e48b553eea7 Mon Sep 17 00:00:00 2001 From: grumbach Date: Tue, 8 Oct 2024 14:34:01 +0900 Subject: [PATCH 4/4] fix: registers missing --- autonomi/src/client/registers.rs | 74 ++++++++++++++++++++++----- autonomi_cli/src/access/network.rs | 2 +- autonomi_cli/src/commands/register.rs | 8 ++- sn_networking/src/error.rs | 3 ++ sn_protocol/src/storage.rs | 3 +- 5 files changed, 73 insertions(+), 17 deletions(-) diff --git a/autonomi/src/client/registers.rs b/autonomi/src/client/registers.rs index a94e198218..6a52fd8820 100644 --- a/autonomi/src/client/registers.rs +++ b/autonomi/src/client/registers.rs @@ -10,7 +10,11 @@ pub use bls::SecretKey as RegisterSecretKey; use sn_evm::Amount; use sn_evm::AttoTokens; +use sn_networking::GetRecordError; +use sn_networking::VerificationKind; +use sn_protocol::storage::RetryStrategy; pub use sn_registers::RegisterAddress; +use tracing::warn; use crate::client::data::PayError; use crate::client::Client; @@ -46,6 +50,8 @@ pub enum RegisterError { Write(#[source] sn_registers::Error), #[error("Failed to sign register")] CouldNotSign(#[source] sn_registers::Error), + #[error("Received invalid quote from node, this node is possibly malfunctioning, try another node by trying another register name")] + InvalidQuote, } #[derive(Clone, Debug)] @@ -85,17 +91,36 @@ impl Client { let key = network_address.to_record_key(); let get_cfg = GetRecordCfg { - get_quorum: Quorum::One, + get_quorum: Quorum::Majority, retry_strategy: None, target_record: None, expected_holders: Default::default(), is_register: true, }; - let record = self.network.get_record_from_network(key, &get_cfg).await?; - - let register: SignedRegister = - try_deserialize_record(&record).map_err(|_| RegisterError::Serialization)?; + let register = match self.network.get_record_from_network(key, &get_cfg).await { + Ok(record) => { + try_deserialize_record(&record).map_err(|_| RegisterError::Serialization)? + } + // manage forked register case + Err(NetworkError::GetRecordError(GetRecordError::SplitRecord { result_map })) => { + let mut registers: Vec = vec![]; + for (_, (record, _)) in result_map { + registers.push( + try_deserialize_record(&record) + .map_err(|_| RegisterError::Serialization)?, + ); + } + let register = registers.iter().fold(registers[0].clone(), |mut acc, x| { + if let Err(e) = acc.merge(x) { + warn!("Ignoring forked register as we failed to merge conflicting registers at {}: {e}", x.address()); + } + acc + }); + register + } + Err(e) => Err(e)?, + }; // Make sure the fetched record contains valid CRDT operations register @@ -143,11 +168,18 @@ impl Client { expires: None, }; + let get_cfg = GetRecordCfg { + get_quorum: Quorum::Majority, + retry_strategy: Some(RetryStrategy::default()), + target_record: None, + expected_holders: Default::default(), + is_register: true, + }; let put_cfg = PutRecordCfg { put_quorum: Quorum::All, retry_strategy: None, use_put_record_to: None, - verification: None, + verification: Some((VerificationKind::Network, get_cfg)), }; // Store the updated register on the network @@ -211,14 +243,23 @@ impl Client { .map(|(entry_hash, _value)| entry_hash) .collect(); - // TODO: Handle error. let _ = register.write(value.into(), &entries, &owner); let reg_xor = register.address().xorname(); - let (payment_proofs, _) = self.pay(std::iter::once(reg_xor), wallet).await?; - // Should always be there, else it would have failed on the payment step. - let proof = payment_proofs.get(®_xor).expect("Missing proof"); - let payee = proof.to_peer_id_payee().expect("Missing payee Peer ID"); - let signed_register = register.clone().into_signed(&owner).expect("TODO"); + let (payment_proofs, _skipped) = self.pay(std::iter::once(reg_xor), wallet).await?; + let proof = if let Some(proof) = payment_proofs.get(®_xor) { + proof + } else { + // register was skipped, meaning it was already paid for + return Err(RegisterError::Network(NetworkError::RegisterAlreadyExists)); + }; + + let payee = proof + .to_peer_id_payee() + .ok_or(RegisterError::InvalidQuote)?; + let signed_register = register + .clone() + .into_signed(&owner) + .map_err(RegisterError::CouldNotSign)?; let record = Record { key: address.to_record_key(), @@ -232,11 +273,18 @@ impl Client { expires: None, }; + let get_cfg = GetRecordCfg { + get_quorum: Quorum::Majority, + retry_strategy: Some(RetryStrategy::default()), + target_record: None, + expected_holders: Default::default(), + is_register: true, + }; let put_cfg = PutRecordCfg { put_quorum: Quorum::All, retry_strategy: None, use_put_record_to: Some(vec![payee]), - verification: None, + verification: Some((VerificationKind::Network, get_cfg)), }; self.network.put_record(record, &put_cfg).await?; diff --git a/autonomi_cli/src/access/network.rs b/autonomi_cli/src/access/network.rs index bfc77e851f..a480bd25ba 100644 --- a/autonomi_cli/src/access/network.rs +++ b/autonomi_cli/src/access/network.rs @@ -6,8 +6,8 @@ // KIND, either express or implied. Please review the Licences for the specific language governing // permissions and limitations relating to use of the SAFE Network Software. -use autonomi::Multiaddr; use autonomi::EvmNetwork; +use autonomi::Multiaddr; use color_eyre::eyre::eyre; use color_eyre::eyre::Context; use color_eyre::Result; diff --git a/autonomi_cli/src/commands/register.rs b/autonomi_cli/src/commands/register.rs index 7971f145da..f1df7660c6 100644 --- a/autonomi_cli/src/commands/register.rs +++ b/autonomi_cli/src/commands/register.rs @@ -78,7 +78,9 @@ pub async fn edit(address: String, name: bool, value: &str, peers: Vec) -> Result<( } else { RegisterAddress::from_hex(&address) .wrap_err(format!("Failed to parse register address: {address}")) - .with_suggestion(|| "if you want to use the name as the address, run the command with the --name flag")? + .with_suggestion(|| { + "if you want to use the name as the address, run the command with the --name flag" + })? }; println!("Getting register at address: {address}"); diff --git a/sn_networking/src/error.rs b/sn_networking/src/error.rs index 6da5a22d9a..6534c84017 100644 --- a/sn_networking/src/error.rs +++ b/sn_networking/src/error.rs @@ -183,6 +183,9 @@ pub enum NetworkError { #[error("Error setting up behaviour: {0}")] BahviourErr(String), + + #[error("Register already exists at this address")] + RegisterAlreadyExists, } #[cfg(test)] diff --git a/sn_protocol/src/storage.rs b/sn_protocol/src/storage.rs index c0a9007ed0..2935e43fce 100644 --- a/sn_protocol/src/storage.rs +++ b/sn_protocol/src/storage.rs @@ -27,11 +27,12 @@ pub use self::{ /// Chunk/Registers/Spend to be more flexible. /// /// The Duration/Attempts is chosen based on the internal logic. -#[derive(Clone, Debug, Copy)] +#[derive(Clone, Debug, Copy, Default)] pub enum RetryStrategy { /// Quick: Resolves to a 15-second wait or 1 retry attempt. Quick, /// Balanced: Resolves to a 60-second wait or 3 retry attempt. + #[default] Balanced, /// Persistent: Resolves to a 180-second wait or 6 retry attempt. Persistent,