From 289a0c1757ee66e4610457a1ad5ddbf959b6d1ef Mon Sep 17 00:00:00 2001 From: umr1352 Date: Mon, 2 Dec 2024 19:05:27 +0100 Subject: [PATCH 1/5] network name is chain id --- examples/utils/utils.rs | 2 +- identity_iota_core/Cargo.toml | 1 + .../src/network/network_name.rs | 57 ++++++-------- .../src/rebased/client/read_only.rs | 78 +++++++++---------- identity_iota_core/src/rebased/iota/mod.rs | 1 + .../src/rebased/iota/well_known_networks.rs | 73 +++++++++++++++++ identity_iota_core/tests/e2e/client.rs | 10 ++- identity_iota_core/tests/e2e/common.rs | 2 +- 8 files changed, 147 insertions(+), 77 deletions(-) create mode 100644 identity_iota_core/src/rebased/iota/well_known_networks.rs diff --git a/examples/utils/utils.rs b/examples/utils/utils.rs index ca9fd74da9..7e70b2ab2a 100644 --- a/examples/utils/utils.rs +++ b/examples/utils/utils.rs @@ -104,7 +104,7 @@ where }) .and_then(|pkg_str| pkg_str.parse().context("invalid package id"))?; - let read_only_client = IdentityClientReadOnly::new(iota_client, package_id).await?; + let read_only_client = IdentityClientReadOnly::new_with_pkg_id(iota_client, package_id).await?; let signer = StorageSigner::new(storage, generate.key_id, public_key_jwk); diff --git a/identity_iota_core/Cargo.toml b/identity_iota_core/Cargo.toml index e2203ffe95..0a5132d1b6 100644 --- a/identity_iota_core/Cargo.toml +++ b/identity_iota_core/Cargo.toml @@ -45,6 +45,7 @@ secret-storage = { git = "https://github.com/iotaledger/secret-storage.git", tag serde-aux = { version = "4.5.0", optional = true } shared-crypto = { git = "https://github.com/iotaledger/iota.git", package = "shared-crypto", tag = "v0.7.3-rc", optional = true } tokio = { version = "1.29.0", default-features = false, optional = true, features = ["macros", "sync", "rt", "process"] } +phf = { version = "0.11.2", features = ["macros"] } [dev-dependencies] iota-crypto = { version = "0.23", default-features = false, features = ["bip39", "bip39-en"] } diff --git a/identity_iota_core/src/network/network_name.rs b/identity_iota_core/src/network/network_name.rs index 97ce562a95..10ae03039d 100644 --- a/identity_iota_core/src/network/network_name.rs +++ b/identity_iota_core/src/network/network_name.rs @@ -1,13 +1,12 @@ // Copyright 2020-2022 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use std::borrow::Cow; - use core::convert::TryFrom; use core::fmt::Display; use core::fmt::Formatter; use core::ops::Deref; use std::fmt::Debug; +use std::str::FromStr; use serde::Deserialize; use serde::Serialize; @@ -18,21 +17,11 @@ use crate::error::Result; /// Network name compliant with the [`crate::IotaDID`] method specification. #[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)] #[repr(transparent)] -pub struct NetworkName(Cow<'static, str>); +pub struct NetworkName(String); impl NetworkName { /// The maximum length of a network name. - pub const MAX_LENGTH: usize = 6; - - /// Creates a new [`NetworkName`] if the name passes validation. - pub fn try_from(name: T) -> Result - where - T: Into>, - { - let name_cow: Cow<'static, str> = name.into(); - Self::validate_network_name(&name_cow)?; - Ok(Self(name_cow)) - } + pub const MAX_LENGTH: usize = 8; /// Validates whether a string is a spec-compliant IOTA DID [`NetworkName`]. pub fn validate_network_name(name: &str) -> Result<()> { @@ -52,33 +41,34 @@ impl AsRef for NetworkName { } } -impl From for Cow<'static, str> { - fn from(network_name: NetworkName) -> Self { - network_name.0 - } -} - impl Deref for NetworkName { - type Target = Cow<'static, str>; + type Target = str; fn deref(&self) -> &Self::Target { &self.0 } } -impl TryFrom<&'static str> for NetworkName { +impl TryFrom for NetworkName { type Error = Error; - - fn try_from(name: &'static str) -> Result { - Self::try_from(Cow::Borrowed(name)) + fn try_from(value: String) -> Result { + Self::validate_network_name(&value)?; + Ok(Self(value)) } } -impl TryFrom for NetworkName { +impl<'a> TryFrom<&'a str> for NetworkName { type Error = Error; + fn try_from(value: &'a str) -> Result { + value.to_string().try_into() + } +} - fn try_from(name: String) -> Result { - Self::try_from(Cow::Owned(name)) +impl FromStr for NetworkName { + type Err = Error; + fn from_str(name: &str) -> Result { + Self::validate_network_name(name)?; + Ok(Self(name.to_string())) } } @@ -98,15 +88,14 @@ impl Display for NetworkName { mod tests { use super::*; - // Rules are: at least one character, at most six characters and may only contain digits and/or lowercase ascii + // Rules are: at least one character, at most eight characters and may only contain digits and/or lowercase ascii // characters. - const VALID_NETWORK_NAMES: [&str; 12] = [ - "main", "dev", "smr", "rms", "test", "foo", "foobar", "123456", "0", "foo42", "bar123", "42foo", + const VALID_NETWORK_NAMES: &[&str] = &[ + "main", "dev", "smr", "rms", "test", "foo", "foobar", "123456", "0", "foo42", "bar123", "42foo", "1234567", + "foobar0", ]; - const INVALID_NETWORK_NAMES: [&str; 10] = [ - "Main", "fOo", "deV", "féta", "", " ", "foo ", " foo", "1234567", "foobar0", - ]; + const INVALID_NETWORK_NAMES: &[&str] = &["Main", "fOo", "deV", "féta", "", " ", "foo ", " foo"]; #[test] fn valid_validate_network_name() { diff --git a/identity_iota_core/src/rebased/client/read_only.rs b/identity_iota_core/src/rebased/client/read_only.rs index 08395aeb43..ead264fd3d 100644 --- a/identity_iota_core/src/rebased/client/read_only.rs +++ b/identity_iota_core/src/rebased/client/read_only.rs @@ -6,6 +6,7 @@ use std::ops::Deref; use std::pin::Pin; use std::str::FromStr; +use crate::rebased::iota; use crate::IotaDID; use crate::IotaDocument; use crate::NetworkName; @@ -33,8 +34,6 @@ use crate::rebased::migration::lookup; use crate::rebased::migration::Identity; use crate::rebased::Error; -const UNKNOWN_NETWORK_HRP: &str = "unknwn"; - /// An [`IotaClient`] enriched with identity-related /// functionalities. #[derive(Clone)] @@ -71,13 +70,40 @@ impl IdentityClientReadOnly { self.migration_registry_id } + /// Attempts to create a new [`IdentityClientReadOnly`] from a given [`IotaClient`]. + + /// # Failures + /// This function fails if the provided `iota_client` is connected to an unrecognized + /// network. + /// + /// # Notes + /// When trying to connect to a local or unofficial network prefer using + /// [`IdentityClientReadOnly::new_with_pkg_id`]. + pub async fn new(iota_client: IotaClient) -> Result { + let network = network_id(&iota_client).await?; + let metadata = iota::well_known_networks::network_metadata(&network).ok_or_else(|| { + Error::InvalidConfig(format!( + "unrecognized network \"{network}\". Use `new_with_pkg_id` instead." + )) + })?; + + let pkg_id = metadata.latest_pkg_id(); + + Ok(IdentityClientReadOnly { + iota_client, + iota_identity_pkg_id: pkg_id, + migration_registry_id: metadata.migration_registry(), + network, + }) + } + /// Attempts to create a new [`IdentityClientReadOnly`] from /// the given [`IotaClient`]. - pub async fn new(iota_client: IotaClient, iota_identity_pkg_id: ObjectID) -> Result { + pub async fn new_with_pkg_id(iota_client: IotaClient, iota_identity_pkg_id: ObjectID) -> Result { let IdentityPkgMetadata { migration_registry_id, .. } = identity_pkg_metadata(&iota_client, iota_identity_pkg_id).await?; - let network = get_client_network(&iota_client).await?; + let network = network_id(&iota_client).await?; Ok(Self { iota_client, iota_identity_pkg_id, @@ -86,21 +112,6 @@ impl IdentityClientReadOnly { }) } - /// Same as [`Self::new`], but if the network isn't recognized among IOTA's official networks, - /// the provided `network_name` will be used. - pub async fn new_with_network_name( - iota_client: IotaClient, - iota_identity_pkg_id: ObjectID, - network_name: NetworkName, - ) -> Result { - let mut identity_client = Self::new(iota_client, iota_identity_pkg_id).await?; - if identity_client.network.as_ref() == UNKNOWN_NETWORK_HRP { - identity_client.network = network_name; - } - - Ok(identity_client) - } - /// Resolves a _Move_ Object of ID `id` and parses it to a value of type `T`. pub async fn get_object_by_id(&self, id: ObjectID) -> Result where @@ -202,9 +213,17 @@ impl IdentityClientReadOnly { } } +async fn network_id(iota_client: &IotaClient) -> Result { + let network_id = iota_client + .read_api() + .get_chain_identifier() + .await + .map_err(|e| Error::RpcError(e.to_string()))?; + Ok(network_id.try_into().expect("chain ID is a valid network name")) +} + #[derive(Debug)] struct IdentityPkgMetadata { - _package_id: ObjectID, migration_registry_id: ObjectID, } @@ -214,24 +233,6 @@ struct MigrationRegistryCreatedEvent { id: ObjectID, } -async fn get_client_network(client: &IotaClient) -> Result { - let network_id = client - .read_api() - .get_chain_identifier() - .await - .map_err(|e| Error::RpcError(e.to_string()))?; - - // TODO: add entries when iota_identity package is published to well-known networks. - #[allow(clippy::match_single_binding)] - let network_hrp = match &network_id { - // "89c3eeec" => NetworkName::try_from("iota").unwrap(), - // "fe12a865" => NetworkName::try_from("atoi").unwrap(), - _ => NetworkName::try_from(UNKNOWN_NETWORK_HRP).unwrap(), // Unrecognized network - }; - - Ok(network_hrp) -} - // TODO: remove argument `package_id` and use `EventFilter::MoveEventField` to find the beacon event and thus the // package id. // TODO: authenticate the beacon event with though sender's ID. @@ -271,7 +272,6 @@ async fn identity_pkg_metadata(iota_client: &IotaClient, package_id: ObjectID) - Ok(IdentityPkgMetadata { migration_registry_id: registry_id, - _package_id: package_id, }) } diff --git a/identity_iota_core/src/rebased/iota/mod.rs b/identity_iota_core/src/rebased/iota/mod.rs index 2daa1c8490..fffb2b8c38 100644 --- a/identity_iota_core/src/rebased/iota/mod.rs +++ b/identity_iota_core/src/rebased/iota/mod.rs @@ -3,3 +3,4 @@ pub(crate) mod move_calls; pub(crate) mod types; +pub(crate) mod well_known_networks; diff --git a/identity_iota_core/src/rebased/iota/well_known_networks.rs b/identity_iota_core/src/rebased/iota/well_known_networks.rs new file mode 100644 index 0000000000..3e3b74a220 --- /dev/null +++ b/identity_iota_core/src/rebased/iota/well_known_networks.rs @@ -0,0 +1,73 @@ +use iota_sdk::types::base_types::ObjectID; +use phf::{phf_map, Map}; + +/// A Mapping `NetworkID` -> metadata needed by the library. +pub(crate) static IOTA_NETWORKS: Map<&str, IdentityNetworkMetadata> = phf_map! { + // devnet + "e678123a" => IdentityNetworkMetadata::new( + &["0xf4e01655b0906ecd3d2bbf3dab03a77acdc13662d07edabce502a9087c122a39"], + "0x816420bd276f9d89ac77bbefa40ba78552760115f1644d84f870db7612088ca8", + ), +}; + +/// `iota_identity` package information for a given network. +#[derive(Debug)] +pub(crate) struct IdentityNetworkMetadata { + /// `package[0]` is the current version, `package[1]` + /// is the version before, and so forth. + pub package: &'static [&'static str], + pub migration_registry: &'static str, +} + +/// Returns the [`IdentityNetworkMetadata`] for a given network, if any. +pub(crate) fn network_metadata(network_id: &str) -> Option<&'static IdentityNetworkMetadata> { + IOTA_NETWORKS.get(network_id) +} + +impl IdentityNetworkMetadata { + const fn new(pkgs: &'static [&'static str], migration_registry: &'static str) -> Self { + assert!(!pkgs.is_empty()); + Self { + package: pkgs, + migration_registry, + } + } + + /// Returns the latest `IotaIdentity` package ID on this network. + pub(crate) fn latest_pkg_id(&self) -> ObjectID { + self + .package + .first() + .expect("a package was published") + .parse() + .expect("valid package ID") + } + + /// Returns the ID for the `MigrationRegistry` on this network. + pub(crate) fn migration_registry(&self) -> ObjectID { + self.migration_registry.parse().expect("valid ObjectID") + } +} + +#[cfg(test)] +mod tests { + use std::ops::Deref; + + use crate::rebased::{client::IdentityClientReadOnly, migration::get_identity}; + + #[tokio::test] + async fn devnet_did_has_right_network_name() -> anyhow::Result<()> { + let iota_client = iota_sdk::IotaClientBuilder::default().build_devnet().await?; + let identity_client = IdentityClientReadOnly::new(iota_client).await?; + let identity = get_identity( + &identity_client, + "0x867b7b3ff149e78216de81339b4d717696ce3089d22fc58b3eeb3c18f1778dfc".parse()?, + ) + .await? + .expect("identity exists on-chain"); + + assert_eq!(identity.deref().id().network_str(), identity_client.network().as_ref()); + + Ok(()) + } +} diff --git a/identity_iota_core/tests/e2e/client.rs b/identity_iota_core/tests/e2e/client.rs index 104809c9bb..3971cc485a 100644 --- a/identity_iota_core/tests/e2e/client.rs +++ b/identity_iota_core/tests/e2e/client.rs @@ -1,6 +1,8 @@ // Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +use std::ops::Deref; + use crate::common::get_client as get_test_client; use crate::common::TEST_DOC; use identity_iota_core::rebased::migration; @@ -11,11 +13,15 @@ async fn can_create_an_identity() -> anyhow::Result<()> { let test_client = get_test_client().await?; let identity_client = test_client.new_user_client().await?; - let _identity = identity_client + let identity = identity_client .create_identity(TEST_DOC) .finish() .execute(&identity_client) - .await?; + .await? + .output; + + let did = identity.deref().id(); + assert_eq!(did.network_str(), identity_client.network().as_ref()); Ok(()) } diff --git a/identity_iota_core/tests/e2e/common.rs b/identity_iota_core/tests/e2e/common.rs index 025fa3c1e5..f41be9a3c0 100644 --- a/identity_iota_core/tests/e2e/common.rs +++ b/identity_iota_core/tests/e2e/common.rs @@ -82,7 +82,7 @@ pub async fn get_client() -> anyhow::Result { request_funds(&address).await?; let storage = Arc::new(Storage::new(JwkMemStore::new(), KeyIdMemstore::new())); - let identity_client = IdentityClientReadOnly::new(client, package_id).await?; + let identity_client = IdentityClientReadOnly::new_with_pkg_id(client, package_id).await?; Ok(TestClient { client: identity_client, From c1f97d41def4d05ad9a674efd2d6fbfcc3ef7096 Mon Sep 17 00:00:00 2001 From: umr1352 Date: Wed, 4 Dec 2024 15:05:44 +0100 Subject: [PATCH 2/5] add package metadata for devnet --- identity_iota_core/src/rebased/iota/well_known_networks.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/identity_iota_core/src/rebased/iota/well_known_networks.rs b/identity_iota_core/src/rebased/iota/well_known_networks.rs index 3e3b74a220..fa19eac562 100644 --- a/identity_iota_core/src/rebased/iota/well_known_networks.rs +++ b/identity_iota_core/src/rebased/iota/well_known_networks.rs @@ -5,8 +5,8 @@ use phf::{phf_map, Map}; pub(crate) static IOTA_NETWORKS: Map<&str, IdentityNetworkMetadata> = phf_map! { // devnet "e678123a" => IdentityNetworkMetadata::new( - &["0xf4e01655b0906ecd3d2bbf3dab03a77acdc13662d07edabce502a9087c122a39"], - "0x816420bd276f9d89ac77bbefa40ba78552760115f1644d84f870db7612088ca8", + &["0x156dfa0c4d4e576f5675de7d4bbe161c767947ffceefd7498cb39c406bc1cb67"], + "0x0247da7f3b8708fc1d326f70153c01b7caf52a19a6f42dd3b868ac8777486b11", ), }; From 213d74e1d3ff577ee5d8222d3af582b2a1335ddc Mon Sep 17 00:00:00 2001 From: Enrico Marconi <31142849+UMR1352@users.noreply.github.com> Date: Wed, 4 Dec 2024 15:06:23 +0100 Subject: [PATCH 3/5] Update identity_iota_core/src/rebased/iota/well_known_networks.rs Co-authored-by: wulfraem --- identity_iota_core/src/rebased/iota/well_known_networks.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/identity_iota_core/src/rebased/iota/well_known_networks.rs b/identity_iota_core/src/rebased/iota/well_known_networks.rs index fa19eac562..46ba187c23 100644 --- a/identity_iota_core/src/rebased/iota/well_known_networks.rs +++ b/identity_iota_core/src/rebased/iota/well_known_networks.rs @@ -1,7 +1,7 @@ use iota_sdk::types::base_types::ObjectID; use phf::{phf_map, Map}; -/// A Mapping `NetworkID` -> metadata needed by the library. +/// A Mapping `network_id` -> metadata needed by the library. pub(crate) static IOTA_NETWORKS: Map<&str, IdentityNetworkMetadata> = phf_map! { // devnet "e678123a" => IdentityNetworkMetadata::new( From d3734dc56573884bda5b6bcc3bc531bcd2642ffa Mon Sep 17 00:00:00 2001 From: umr1352 Date: Wed, 4 Dec 2024 15:11:00 +0100 Subject: [PATCH 4/5] remove test that uses wrong identity --- .../src/rebased/iota/well_known_networks.rs | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/identity_iota_core/src/rebased/iota/well_known_networks.rs b/identity_iota_core/src/rebased/iota/well_known_networks.rs index 46ba187c23..415cab1cce 100644 --- a/identity_iota_core/src/rebased/iota/well_known_networks.rs +++ b/identity_iota_core/src/rebased/iota/well_known_networks.rs @@ -48,26 +48,3 @@ impl IdentityNetworkMetadata { self.migration_registry.parse().expect("valid ObjectID") } } - -#[cfg(test)] -mod tests { - use std::ops::Deref; - - use crate::rebased::{client::IdentityClientReadOnly, migration::get_identity}; - - #[tokio::test] - async fn devnet_did_has_right_network_name() -> anyhow::Result<()> { - let iota_client = iota_sdk::IotaClientBuilder::default().build_devnet().await?; - let identity_client = IdentityClientReadOnly::new(iota_client).await?; - let identity = get_identity( - &identity_client, - "0x867b7b3ff149e78216de81339b4d717696ce3089d22fc58b3eeb3c18f1778dfc".parse()?, - ) - .await? - .expect("identity exists on-chain"); - - assert_eq!(identity.deref().id().network_str(), identity_client.network().as_ref()); - - Ok(()) - } -} From 9a4d60fcb3a28f26cd8ce776b4a753bedb79209d Mon Sep 17 00:00:00 2001 From: umr1352 Date: Wed, 4 Dec 2024 15:16:00 +0100 Subject: [PATCH 5/5] add devnet connection test --- identity_iota_core/src/rebased/client/read_only.rs | 5 ++--- .../src/rebased/iota/well_known_networks.rs | 13 +++++++++++++ .../src/rebased/migration/identity.rs | 4 +++- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/identity_iota_core/src/rebased/client/read_only.rs b/identity_iota_core/src/rebased/client/read_only.rs index 3247f29130..7d85a5fc8d 100644 --- a/identity_iota_core/src/rebased/client/read_only.rs +++ b/identity_iota_core/src/rebased/client/read_only.rs @@ -14,9 +14,9 @@ use anyhow::anyhow; use anyhow::Context as _; use futures::stream::FuturesUnordered; +use futures::StreamExt as _; use identity_core::common::Url; use identity_did::DID; -use futures::StreamExt as _; use iota_sdk::rpc_types::EventFilter; use iota_sdk::rpc_types::IotaData as _; use iota_sdk::rpc_types::IotaObjectData; @@ -199,8 +199,7 @@ impl IdentityClientReadOnly { /// Resolves an [`Identity`] from its ID `object_id`. pub async fn get_identity(&self, object_id: ObjectID) -> Result { // spawn all checks - let all_futures = - FuturesUnordered::, Error>> + Send>>>::new(); + let all_futures = FuturesUnordered::, Error>> + Send>>>::new(); all_futures.push(Box::pin(resolve_new(self, object_id))); all_futures.push(Box::pin(resolve_migrated(self, object_id))); all_futures.push(Box::pin(resolve_unmigrated(self, object_id))); diff --git a/identity_iota_core/src/rebased/iota/well_known_networks.rs b/identity_iota_core/src/rebased/iota/well_known_networks.rs index 415cab1cce..8243a76b46 100644 --- a/identity_iota_core/src/rebased/iota/well_known_networks.rs +++ b/identity_iota_core/src/rebased/iota/well_known_networks.rs @@ -48,3 +48,16 @@ impl IdentityNetworkMetadata { self.migration_registry.parse().expect("valid ObjectID") } } + +#[cfg(test)] +mod test { + use iota_sdk::IotaClientBuilder; + + use crate::rebased::client::IdentityClientReadOnly; + + #[tokio::test] + async fn identity_client_connection_to_devnet_works() -> anyhow::Result<()> { + IdentityClientReadOnly::new(IotaClientBuilder::default().build_devnet().await?).await?; + Ok(()) + } +} diff --git a/identity_iota_core/src/rebased/migration/identity.rs b/identity_iota_core/src/rebased/migration/identity.rs index e5c21c665f..42556e92bb 100644 --- a/identity_iota_core/src/rebased/migration/identity.rs +++ b/identity_iota_core/src/rebased/migration/identity.rs @@ -533,7 +533,9 @@ impl Transaction for CreateIdentityTx { threshold, controllers, } = self.0; - let did_doc = StateMetadataDocument::from(did_doc).pack(StateMetadataEncoding::default()).map_err(|e| Error::DidDocSerialization(e.to_string()))?; + let did_doc = StateMetadataDocument::from(did_doc) + .pack(StateMetadataEncoding::default()) + .map_err(|e| Error::DidDocSerialization(e.to_string()))?; let programmable_transaction = if controllers.is_empty() { move_calls::identity::new(&did_doc, client.package_id())? } else {