diff --git a/.vscode/settings.json b/.vscode/settings.json index e083a622..f01e03c0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,7 @@ { "cSpell.words": [ "analyser", + "Appendable", "Banksy", "bdfs", "Fulfillable", diff --git a/src/factor_instances_provider/agnostic_paths/derivation_preset.rs b/src/factor_instances_provider/agnostic_paths/derivation_preset.rs new file mode 100644 index 00000000..bac1ca64 --- /dev/null +++ b/src/factor_instances_provider/agnostic_paths/derivation_preset.rs @@ -0,0 +1,93 @@ +use crate::prelude::*; + +/// Derivation Presets are Network agnostic and Index agnostic +/// "templates" for DerivationPaths. +#[derive(Clone, Copy, Hash, PartialEq, Eq, enum_iterator::Sequence, derive_more::Debug)] +pub enum DerivationPreset { + /// Used to form DerivationPaths used to derive FactorInstances + /// for "veci": Virtual Entity Creating (Factor)Instance for accounts. + /// `(EntityKind::Account, KeySpace::Unsecurified, KeyKind::TransactionSigning)` + #[debug("A-VECI")] + AccountVeci, + + /// Used to form DerivationPaths used to derive FactorInstances + /// for "mfa" to securify accounts. + /// `(EntityKind::Account, KeySpace::Securified, KeyKind::TransactionSigning)` + #[debug("A-MFA")] + AccountMfa, + + /// Used to form DerivationPaths used to derive FactorInstances + /// for "veci": Virtual Entity Creating (Factor)Instance for personas. + /// `(EntityKind::Identity, KeySpace::Unsecurified, KeyKind::TransactionSigning)` + #[debug("I-VECI")] + IdentityVeci, + + /// Used to form DerivationPaths used to derive FactorInstances + /// for "mfa" to securify personas. + /// `(EntityKind::Identity, KeySpace::Securified, KeyKind::TransactionSigning)` + #[debug("I-MFA")] + IdentityMfa, +} + +// ============= +// Construction +// ============= +impl DerivationPreset { + /// All DerivationPreset's, used to fill cache. + pub fn all() -> IndexSet { + enum_iterator::all::().collect() + } + + /// Selects a `DerivationPreset` for veci based on `CAP26EntityKind`, + /// i.e. either `DerivationPreset::AccountVeci` or `DerivationPreset::IdentityVeci`. + pub fn veci_entity_kind(entity_kind: CAP26EntityKind) -> Self { + match entity_kind { + CAP26EntityKind::Account => Self::AccountVeci, + CAP26EntityKind::Identity => Self::IdentityVeci, + } + } + + /// Selects a `DerivationPreset` for MFA based on `CAP26EntityKind`, + /// i.e. either `DerivationPreset::AccountMfa` or `DerivationPreset::IdentityMfa`. + pub fn mfa_entity_kind(entity_kind: CAP26EntityKind) -> Self { + match entity_kind { + CAP26EntityKind::Account => Self::AccountMfa, + CAP26EntityKind::Identity => Self::IdentityMfa, + } + } +} + +// ============= +// Instance Methods +// ============= +impl DerivationPreset { + /// Returns the `CAP26EntityKind` of the `DerivationPreset`. + pub fn entity_kind(&self) -> CAP26EntityKind { + match self { + Self::AccountVeci | Self::AccountMfa => CAP26EntityKind::Account, + Self::IdentityVeci | Self::IdentityMfa => CAP26EntityKind::Identity, + } + } + + /// Returns the `CAP26KeyKind` of the `DerivationPreset`. + pub fn key_kind(&self) -> CAP26KeyKind { + match self { + Self::AccountVeci | Self::IdentityVeci | Self::AccountMfa | Self::IdentityMfa => { + CAP26KeyKind::TransactionSigning + } + } + } + + /// Returns the `KeySpace` of the `DerivationPreset`. + pub fn key_space(&self) -> KeySpace { + match self { + Self::AccountVeci | Self::IdentityVeci => KeySpace::Unsecurified, + Self::AccountMfa | Self::IdentityMfa => KeySpace::Securified, + } + } + + /// Maps a DerivationPreset to a `IndexAgnosticPath` which is network aware. + pub fn index_agnostic_path_on_network(&self, network_id: NetworkID) -> IndexAgnosticPath { + IndexAgnosticPath::from((network_id, *self)) + } +} diff --git a/src/factor_instances_provider/agnostic_paths/index_agnostic_path.rs b/src/factor_instances_provider/agnostic_paths/index_agnostic_path.rs new file mode 100644 index 00000000..4e0e2138 --- /dev/null +++ b/src/factor_instances_provider/agnostic_paths/index_agnostic_path.rs @@ -0,0 +1,122 @@ +use crate::prelude::*; + +use super::quantities; + +/// A DerivationPath which is not indexed. On a specific network. +#[derive(Clone, Copy, Hash, PartialEq, Eq, derive_more::Debug, derive_more::Display)] +#[display("{}/{}/{}/?{}", network_id, entity_kind, key_kind, key_space.indicator())] +#[debug("{:?}/{:?}/{:?}/?{}", network_id, entity_kind, key_kind, key_space.indicator())] +pub struct IndexAgnosticPath { + pub network_id: NetworkID, + pub entity_kind: CAP26EntityKind, + pub key_kind: CAP26KeyKind, + pub key_space: KeySpace, +} + +impl IndexAgnosticPath { + pub fn new( + network_id: NetworkID, + entity_kind: CAP26EntityKind, + key_kind: CAP26KeyKind, + key_space: KeySpace, + ) -> Self { + Self { + network_id, + entity_kind, + key_kind, + key_space, + } + } +} + +impl From<(NetworkID, DerivationPreset)> for IndexAgnosticPath { + fn from((network_id, agnostic_path): (NetworkID, DerivationPreset)) -> Self { + Self::new( + network_id, + agnostic_path.entity_kind(), + agnostic_path.key_kind(), + agnostic_path.key_space(), + ) + } +} + +impl TryFrom for DerivationPreset { + type Error = CommonError; + /// Tries to convert an IndexAgnosticPath to a DerivationPreset, + /// is failing if the path is not a standard DerivationPreset + fn try_from(value: IndexAgnosticPath) -> Result { + match (value.entity_kind, value.key_kind, value.key_space) { + ( + CAP26EntityKind::Account, + CAP26KeyKind::TransactionSigning, + KeySpace::Unsecurified, + ) => Ok(DerivationPreset::AccountVeci), + ( + CAP26EntityKind::Identity, + CAP26KeyKind::TransactionSigning, + KeySpace::Unsecurified, + ) => Ok(DerivationPreset::IdentityVeci), + (CAP26EntityKind::Account, CAP26KeyKind::TransactionSigning, KeySpace::Securified) => { + Ok(DerivationPreset::AccountMfa) + } + (CAP26EntityKind::Identity, CAP26KeyKind::TransactionSigning, KeySpace::Securified) => { + Ok(DerivationPreset::IdentityMfa) + } + _ => Err(CommonError::NonStandardDerivationPath), + } + } +} + +impl From<(IndexAgnosticPath, HDPathComponent)> for DerivationPath { + fn from((path, index): (IndexAgnosticPath, HDPathComponent)) -> Self { + assert_eq!(index.key_space(), path.key_space); + Self::new(path.network_id, path.entity_kind, path.key_kind, index) + } +} + +impl DerivationPath { + pub fn agnostic(&self) -> IndexAgnosticPath { + IndexAgnosticPath { + network_id: self.network_id, + entity_kind: self.entity_kind, + key_kind: self.key_kind, + key_space: self.key_space(), + } + } +} +impl HierarchicalDeterministicFactorInstance { + pub fn agnostic_path(&self) -> IndexAgnosticPath { + self.derivation_path().agnostic() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + type Sut = IndexAgnosticPath; + + #[test] + fn try_from_success() { + NetworkID::all().into_iter().for_each(|n| { + let f = |preset: DerivationPreset| { + let sut = preset.index_agnostic_path_on_network(n); + let back_again = DerivationPreset::try_from(sut).unwrap(); + assert_eq!(back_again, preset); + }; + + DerivationPreset::all().into_iter().for_each(|p| f(p)); + }); + } + + #[test] + fn try_from_fail() { + let path = Sut::new( + NetworkID::Stokenet, + CAP26EntityKind::Account, + CAP26KeyKind::AuthenticationSigning, + KeySpace::Unsecurified, + ); + assert!(DerivationPreset::try_from(path).is_err()); + } +} diff --git a/src/factor_instances_provider/agnostic_paths/mod.rs b/src/factor_instances_provider/agnostic_paths/mod.rs new file mode 100644 index 00000000..15238a79 --- /dev/null +++ b/src/factor_instances_provider/agnostic_paths/mod.rs @@ -0,0 +1,9 @@ +mod derivation_preset; +mod index_agnostic_path; +mod quantified_derivation_preset; +mod quantities; + +pub use derivation_preset::*; +pub use index_agnostic_path::*; +pub use quantified_derivation_preset::*; +pub use quantities::*; diff --git a/src/factor_instances_provider/agnostic_paths/quantified_derivation_preset.rs b/src/factor_instances_provider/agnostic_paths/quantified_derivation_preset.rs new file mode 100644 index 00000000..250ef7c8 --- /dev/null +++ b/src/factor_instances_provider/agnostic_paths/quantified_derivation_preset.rs @@ -0,0 +1,17 @@ +use crate::prelude::*; + +#[derive(Clone, Copy, Hash, PartialEq, Eq, derive_more::Debug)] +#[debug("🎯: {:?} #{}", self.derivation_preset, self.quantity)] +pub struct QuantifiedDerivationPreset { + pub derivation_preset: DerivationPreset, + pub quantity: usize, +} + +impl QuantifiedDerivationPreset { + pub fn new(derivation_preset: DerivationPreset, quantity: usize) -> Self { + Self { + derivation_preset, + quantity, + } + } +} diff --git a/src/factor_instances_provider/agnostic_paths/quantities.rs b/src/factor_instances_provider/agnostic_paths/quantities.rs new file mode 100644 index 00000000..e6349fbf --- /dev/null +++ b/src/factor_instances_provider/agnostic_paths/quantities.rs @@ -0,0 +1,4 @@ +use crate::prelude::*; + +/// The quantity of DerivationPreset's to fill cache with. +pub const CACHE_FILLING_QUANTITY: usize = 30; diff --git a/src/factor_instances_provider/mod.rs b/src/factor_instances_provider/mod.rs new file mode 100644 index 00000000..294823e5 --- /dev/null +++ b/src/factor_instances_provider/mod.rs @@ -0,0 +1,7 @@ +mod agnostic_paths; +mod next_index_assigner; +mod provider; + +pub use agnostic_paths::*; +pub use next_index_assigner::*; +pub use provider::*; diff --git a/src/factor_instances_provider/next_index_assigner/mod.rs b/src/factor_instances_provider/next_index_assigner/mod.rs new file mode 100644 index 00000000..a687d9d3 --- /dev/null +++ b/src/factor_instances_provider/next_index_assigner/mod.rs @@ -0,0 +1,11 @@ +mod next_derivation_entity_index_assigner; +mod next_derivation_entity_index_cache_analyzing_assigner; +mod next_derivation_entity_index_profile_analyzing_assigner; +mod next_derivation_entity_index_with_ephemeral_offsets; +mod next_derivation_entity_index_with_ephemeral_offsets_for_factor_source; + +pub use next_derivation_entity_index_assigner::*; +pub use next_derivation_entity_index_cache_analyzing_assigner::*; +pub use next_derivation_entity_index_profile_analyzing_assigner::*; +pub use next_derivation_entity_index_with_ephemeral_offsets::*; +pub use next_derivation_entity_index_with_ephemeral_offsets_for_factor_source::*; diff --git a/src/factor_instances_provider/next_index_assigner/next_derivation_entity_index_assigner.rs b/src/factor_instances_provider/next_index_assigner/next_derivation_entity_index_assigner.rs new file mode 100644 index 00000000..9bb67d40 --- /dev/null +++ b/src/factor_instances_provider/next_index_assigner/next_derivation_entity_index_assigner.rs @@ -0,0 +1,84 @@ +use crate::prelude::*; + +/// An assigner of derivation entity indices, used by the FactorInstancesProvider +/// to map `IndexAgnosticPath` -> `DerivationPath` for some FactorSource on +/// some NetworkID. +/// +/// This assigner works with the: +/// * cache +/// * profile +/// * local offsets +/// +/// More specifically the assigner's `next` method performs approximately this +/// operation: +/// +/// ```ignore +/// pub fn next( +/// &mut self, +/// fs_id: FactorSourceIDFromHash, +/// path: IndexAgnosticPath, +/// ) -> Result { +/// let next_from_cache = self.cache_analyzing.next(fs_id, path).unwrap_or(0); +/// let next_from_profile = self.profile_analyzing.next(fs_id, path).unwrap_or(0); +/// +/// let max_index = std::cmp::max(next_from_profile, next_from_cache); +/// let ephemeral_offset = self.ephemeral_offsets.reserve() +/// +/// max_index + ephemeral_offset +/// ``` +pub struct NextDerivationEntityIndexAssigner { + profile_analyzing: NextDerivationEntityIndexProfileAnalyzingAssigner, + cache_analyzing: NextDerivationEntityIndexCacheAnalyzingAssigner, + ephemeral_offsets: NextDerivationEntityIndexWithEphemeralOffsets, +} + +impl NextDerivationEntityIndexAssigner { + pub fn new( + network_id: NetworkID, + profile: impl Into>, + cache: FactorInstancesCache, + ) -> Self { + let profile_analyzing = + NextDerivationEntityIndexProfileAnalyzingAssigner::new(network_id, profile); + let cache_analyzing = NextDerivationEntityIndexCacheAnalyzingAssigner::new(cache); + let ephemeral_offsets = NextDerivationEntityIndexWithEphemeralOffsets::default(); + Self { + profile_analyzing, + cache_analyzing, + ephemeral_offsets, + } + } + + /// Returns the next index for the given `FactorSourceIDFromHash` and + /// `IndexAgnosticPath`, by analyzing the cache, the profile and adding + /// local ephemeral offsets. + pub fn next( + &self, + factor_source_id: FactorSourceIDFromHash, + index_agnostic_path: IndexAgnosticPath, + ) -> Result { + let default_index = HDPathComponent::new_with_key_space_and_base_index( + index_agnostic_path.key_space, + U30::new(0).unwrap(), + ); + + let maybe_next_from_cache = self + .cache_analyzing + .next(factor_source_id, index_agnostic_path)?; + + let next_from_cache = maybe_next_from_cache.unwrap_or(default_index); + let ephemeral = self + .ephemeral_offsets + .reserve(factor_source_id, index_agnostic_path); + + let maybe_next_from_profile = self + .profile_analyzing + .next(factor_source_id, index_agnostic_path)?; + + let next_from_profile = maybe_next_from_profile.unwrap_or(default_index); + + let max_index = std::cmp::max(next_from_profile, next_from_cache); + + max_index.add_n(ephemeral) + } +} diff --git a/src/factor_instances_provider/next_index_assigner/next_derivation_entity_index_cache_analyzing_assigner.rs b/src/factor_instances_provider/next_index_assigner/next_derivation_entity_index_cache_analyzing_assigner.rs new file mode 100644 index 00000000..45b09193 --- /dev/null +++ b/src/factor_instances_provider/next_index_assigner/next_derivation_entity_index_cache_analyzing_assigner.rs @@ -0,0 +1,37 @@ +use crate::prelude::*; + +pub struct NextDerivationEntityIndexCacheAnalyzingAssigner { + cache: FactorInstancesCache, +} +impl NextDerivationEntityIndexCacheAnalyzingAssigner { + pub fn new(cache: FactorInstancesCache) -> Self { + Self { cache } + } + + fn max( + &self, + factor_source_id: FactorSourceIDFromHash, + index_agnostic_path: IndexAgnosticPath, + ) -> Option { + self.cache + .max_index_for(factor_source_id, index_agnostic_path) + } + + /// Returns the next index for the given `FactorSourceIDFromHash` and + /// `IndexAgnosticPath`, by analyzing the cache. In case of read failure + /// will this method return `Err`, if the cache did not contain any data for + /// the given `FactorSourceIDFromHash` and `IndexAgnosticPath`, then `Ok(None)` is returned. + /// + /// If some index was found, this method returns `max + 1`. + /// + /// Can also fail if addition of one would overflow. + pub fn next( + &self, + factor_source_id: FactorSourceIDFromHash, + index_agnostic_path: IndexAgnosticPath, + ) -> Result> { + let max = self.max(factor_source_id, index_agnostic_path); + let Some(max) = max else { return Ok(None) }; + max.add_one().map(Some) + } +} diff --git a/src/factor_instances_provider/next_index_assigner/next_derivation_entity_index_profile_analyzing_assigner.rs b/src/factor_instances_provider/next_index_assigner/next_derivation_entity_index_profile_analyzing_assigner.rs new file mode 100644 index 00000000..48c3f2ad --- /dev/null +++ b/src/factor_instances_provider/next_index_assigner/next_derivation_entity_index_profile_analyzing_assigner.rs @@ -0,0 +1,432 @@ +use crate::prelude::*; + +/// An analyzer of a `Profile` for some `network_id` (i.e. analyzer of `ProfileNetwork`), +/// reading out the max derivation entity index for Unsecurified/Securified Accounts/Personas +/// for some factor source id. +pub struct NextDerivationEntityIndexProfileAnalyzingAssigner { + network_id: NetworkID, + + /// might be empty + unsecurified_accounts_on_network: IndexSet, + + /// might be empty + securified_accounts_on_network: IndexSet, + + /// might be empty + unsecurified_identities_on_network: IndexSet, + + /// might be empty + securified_identities_on_network: IndexSet, +} + +impl NextDerivationEntityIndexProfileAnalyzingAssigner { + /// `Profile` is optional so that one can use the same initializer from `FactorInstancesProvider`, + /// which accepts an optional Profile. Will just default to empty lists if `None` is passed, + /// effectively making this whole assigner NOOP. + pub fn new(network_id: NetworkID, profile: impl Into>) -> Self { + let profile = profile.into(); + let unsecurified_accounts_on_network = profile + .as_ref() + .map(|p| p.unsecurified_accounts_on_network(network_id)) + .unwrap_or_default(); + + let securified_accounts_on_network = profile + .as_ref() + .map(|p| p.securified_accounts_on_network(network_id)) + .unwrap_or_default(); + + let unsecurified_identities_on_network = profile + .as_ref() + .map(|p| p.unsecurified_identities_on_network(network_id)) + .unwrap_or_default(); + + let securified_identities_on_network = profile + .as_ref() + .map(|p| p.securified_identities_on_network(network_id)) + .unwrap_or_default(); + + Self { + network_id, + unsecurified_accounts_on_network, + securified_accounts_on_network, + unsecurified_identities_on_network, + securified_identities_on_network, + } + } + + fn max_entity_veci( + &self, + factor_source_id: FactorSourceIDFromHash, + entities: impl IntoIterator, + securified_entities: impl IntoIterator, + entity_kind: CAP26EntityKind, + key_space: KeySpace, + ) -> Option { + let max_veci = |vecis: IndexSet| -> Option { + vecis + .into_iter() + .map(|x| x.factor_instance()) + .filter(|f| f.factor_source_id == factor_source_id) + .map(|f| f.derivation_path()) + .map(|p| { + AssertMatches { + network_id: self.network_id, + key_kind: CAP26KeyKind::TransactionSigning, + entity_kind, + key_space, + } + .matches(&p) + }) + .map(|fi| fi.index) + .max() + }; + + let of_unsecurified = max_veci(entities.into_iter().map(|x| x.veci()).collect()); + + // The securified entities might have been originally created - having a veci - + // with the same factor source id. + let of_securified = max_veci( + securified_entities + .into_iter() + .filter_map(|x| x.veci()) + .collect(), + ); + + std::cmp::max(of_unsecurified, of_securified) + } + + /// Returns the Max Derivation Entity Index of Unsecurified Accounts controlled + /// by `factor_source_id`, or `None` if no unsecurified account controlled by that + /// factor source id found. + fn max_account_veci( + &self, + factor_source_id: FactorSourceIDFromHash, + ) -> Option { + self.max_entity_veci( + factor_source_id, + self.unsecurified_accounts_on_network.clone(), + self.securified_accounts_on_network + .clone() + .into_iter() + .map(|x| x.securified_entity_control()), + CAP26EntityKind::Account, + KeySpace::Unsecurified, + ) + } + + /// Returns the Max Derivation Entity Index of Unsecurified Personas controlled + /// by `factor_source_id`, or `None` if no unsecurified persona controlled by that + /// factor source id found. + fn max_identity_veci( + &self, + factor_source_id: FactorSourceIDFromHash, + ) -> Option { + self.max_entity_veci( + factor_source_id, + self.unsecurified_identities_on_network.clone(), + self.securified_identities_on_network + .clone() + .into_iter() + .map(|x| x.securified_entity_control()), + CAP26EntityKind::Identity, + KeySpace::Unsecurified, + ) + } + + /// Returns the Max Derivation Entity Index of Securified Accounts controlled + /// by `factor_source_id`, or `None` if no securified account controlled by that + /// factor source id found. + /// By "controlled by" we mean having a MatrixOfFactorInstances which has that + /// factor in **any role** in its MatrixOfFactorInstances. + fn max_account_mfa(&self, factor_source_id: FactorSourceIDFromHash) -> Option { + self.securified_accounts_on_network + .clone() + .into_iter() + .flat_map(|e: SecurifiedAccount| { + e.highest_derivation_path_index( + factor_source_id, + AssertMatches { + network_id: self.network_id, + key_kind: CAP26KeyKind::TransactionSigning, + entity_kind: CAP26EntityKind::Account, + key_space: KeySpace::Securified, + }, + ) + }) + .max() + } + + /// Returns the Max Derivation Entity Index of Securified Persona controlled + /// by `factor_source_id`, or `None` if no securified persona controlled by that + /// factor source id found. + /// By "controlled by" we mean having a MatrixOfFactorInstances which has that + /// factor in **any role** in its MatrixOfFactorInstances. + fn max_identity_mfa( + &self, + factor_source_id: FactorSourceIDFromHash, + ) -> Option { + self.securified_identities_on_network + .clone() + .into_iter() + .flat_map(|e: SecurifiedPersona| { + e.highest_derivation_path_index( + factor_source_id, + AssertMatches { + network_id: self.network_id, + key_kind: CAP26KeyKind::TransactionSigning, + entity_kind: CAP26EntityKind::Identity, + key_space: KeySpace::Securified, + }, + ) + }) + .max() + } + + /// Finds the "next" derivation entity index `HDPathComponent`, for + /// the given `IndexAgnosticPath` adnd `factor_source_id`, which is `Max + 1`. + /// Returns `None` if `Max` is `None` (see `max_account_veci`, `max_identity_mfa` for more details). + /// + /// Returns `Err` if the addition of one would overflow. + pub fn next( + &self, + factor_source_id: FactorSourceIDFromHash, + agnostic_path: IndexAgnosticPath, + ) -> Result> { + if agnostic_path.network_id != self.network_id { + return Err(CommonError::NetworkDiscrepancy); + } + let derivation_preset = DerivationPreset::try_from(agnostic_path)?; + + let max = match derivation_preset { + DerivationPreset::AccountVeci => self.max_account_veci(factor_source_id), + DerivationPreset::AccountMfa => self.max_account_mfa(factor_source_id), + DerivationPreset::IdentityVeci => self.max_identity_veci(factor_source_id), + DerivationPreset::IdentityMfa => self.max_identity_mfa(factor_source_id), + }; + + let Some(max) = max else { return Ok(None) }; + max.add_one().map(Some) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + type Sut = NextDerivationEntityIndexProfileAnalyzingAssigner; + + #[test] + fn test_network_discrepancy() { + let sut = Sut::new(NetworkID::Mainnet, None); + assert_eq!( + sut.next( + FactorSourceIDFromHash::fs0(), + DerivationPreset::AccountVeci.index_agnostic_path_on_network(NetworkID::Stokenet), + ), + Err(CommonError::NetworkDiscrepancy) + ); + } + + #[test] + fn test_next_account_veci_with_single_at_0_is_1() { + let preset = DerivationPreset::AccountVeci; + let network_id = NetworkID::Mainnet; + let sut = Sut::new( + network_id, + Some(Profile::new(HDFactorSource::all(), [&Account::a0()], [])), + ); + let next = sut + .next( + FactorSourceIDFromHash::fs0(), + preset.index_agnostic_path_on_network(network_id), + ) + .unwrap(); + + assert_eq!( + next, + Some(HDPathComponent::unsecurified_hardening_base_index(1)) + ) + } + + #[test] + fn test_next_account_veci_with_unused_factor_is_none() { + let preset = DerivationPreset::AccountVeci; + let network_id = NetworkID::Mainnet; + let sut = Sut::new( + network_id, + Some(Profile::new(HDFactorSource::all(), [&Account::a0()], [])), + ); + let next = sut + .next( + FactorSourceIDFromHash::fs1(), // <-- UNUSED + preset.index_agnostic_path_on_network(network_id), + ) + .unwrap(); + + assert_eq!(next, None) + } + + #[test] + fn test_next_account_mfa_with_single_unsecurified_is_none() { + let preset = DerivationPreset::AccountMfa; + let network_id = NetworkID::Mainnet; + let sut = Sut::new( + network_id, + Some(Profile::new(HDFactorSource::all(), [&Account::a0()], [])), + ); + let next = sut + .next( + FactorSourceIDFromHash::fs0(), + preset.index_agnostic_path_on_network(network_id), + ) + .unwrap(); + + assert_eq!(next, None) + } + + #[test] + fn test_next_account_veci_with_single_at_8_is_9() { + let preset = DerivationPreset::AccountVeci; + let network_id = NetworkID::Mainnet; + let sut = Sut::new( + network_id, + Some(Profile::new( + HDFactorSource::all(), + [ + &Account::a8(), + &Account::a2(), /* securified, should not interfere */ + ], + [], + )), + ); + let next = sut + .next( + FactorSourceIDFromHash::fs10(), + preset.index_agnostic_path_on_network(network_id), + ) + .unwrap(); + + assert_eq!( + next, + Some(HDPathComponent::unsecurified_hardening_base_index(9)) + ) + } + + #[test] + fn test_next_account_mfa_with_single_at_7_is_8() { + let preset = DerivationPreset::AccountMfa; + let network_id = NetworkID::Mainnet; + let sut = Sut::new( + network_id, + Some(Profile::new( + HDFactorSource::all(), + [ + &Account::a8(), /* unsecurified, should not interfere */ + &Account::a7(), + ], + [], + )), + ); + type F = FactorSourceIDFromHash; + for fid in [F::fs2(), F::fs6(), F::fs7(), F::fs8(), F::fs9()] { + let next = sut + .next(fid, preset.index_agnostic_path_on_network(network_id)) + .unwrap(); + + assert_eq!(next, Some(HDPathComponent::securifying_base_index(8))) + } + } + + #[test] + fn test_next_identity_mfa_with_single_at_7_is_8() { + let preset = DerivationPreset::IdentityMfa; + let network_id = NetworkID::Mainnet; + let sut = Sut::new( + network_id, + Some(Profile::new(HDFactorSource::all(), [], [&Persona::p7()])), + ); + type F = FactorSourceIDFromHash; + for fid in [F::fs2(), F::fs6(), F::fs7(), F::fs8(), F::fs9()] { + let next = sut + .next(fid, preset.index_agnostic_path_on_network(network_id)) + .unwrap(); + + assert_eq!(next, Some(HDPathComponent::securifying_base_index(8))) + } + } + + #[test] + fn test_next_identity_veci_with_single_at_1_is_2() { + let preset = DerivationPreset::IdentityVeci; + let network_id = NetworkID::Mainnet; + let sut = Sut::new( + network_id, + Some(Profile::new( + HDFactorSource::all(), + [], + [ + &Persona::p7(), /* securified should not interfere */ + &Persona::p1(), + ], + )), + ); + let next = sut + .next( + FactorSourceIDFromHash::fs1(), + preset.index_agnostic_path_on_network(network_id), + ) + .unwrap(); + + assert_eq!( + next, + Some(HDPathComponent::unsecurified_hardening_base_index(2)) + ) + } + + #[test] + fn test_next_account_veci_with_non_contiguous_at_0_1_7_is_8() { + let fsid = FactorSourceIDFromHash::fs0(); + + let fi0 = HierarchicalDeterministicFactorInstance::mainnet_tx( + CAP26EntityKind::Account, + HDPathComponent::unsecurified_hardening_base_index(0), + fsid, + ); + let fi1 = HierarchicalDeterministicFactorInstance::mainnet_tx( + CAP26EntityKind::Account, + HDPathComponent::unsecurified_hardening_base_index(1), + fsid, + ); + + let fi7 = HierarchicalDeterministicFactorInstance::mainnet_tx( + CAP26EntityKind::Account, + HDPathComponent::unsecurified_hardening_base_index(7), + fsid, + ); + + let network_id = NetworkID::Mainnet; + let accounts = [fi0, fi1, fi7].map(|fi| { + Account::new( + "acco", + AccountAddress::new(network_id, fi.public_key_hash()), + EntitySecurityState::Unsecured(fi), + ThirdPartyDepositPreference::default(), + ) + }); + let sut = Sut::new( + network_id, + Some(Profile::new(HDFactorSource::all(), &accounts, [])), + ); + let next = sut + .next( + fsid, + DerivationPreset::AccountVeci.index_agnostic_path_on_network(network_id), + ) + .unwrap(); + + assert_eq!( + next, + Some(HDPathComponent::unsecurified_hardening_base_index(8)) + ) + } +} diff --git a/src/factor_instances_provider/next_index_assigner/next_derivation_entity_index_with_ephemeral_offsets.rs b/src/factor_instances_provider/next_index_assigner/next_derivation_entity_index_with_ephemeral_offsets.rs new file mode 100644 index 00000000..d2f930d1 --- /dev/null +++ b/src/factor_instances_provider/next_index_assigner/next_derivation_entity_index_with_ephemeral_offsets.rs @@ -0,0 +1,113 @@ +use crate::prelude::*; + +/// Essentially a map of `NextDerivationEntityIndexWithEphemeralOffsetsForFactorSource` +/// ephemeral offsets used by `NextDerivationEntityIndexAssigner` +/// to add ephemeral offsets to next index calculations. +#[derive(Default, Debug)] +pub struct NextDerivationEntityIndexWithEphemeralOffsets { + ephemeral_offsets_per_factor_source: RwLock< + HashMap< + FactorSourceIDFromHash, + NextDerivationEntityIndexWithEphemeralOffsetsForFactorSource, + >, + >, +} + +impl NextDerivationEntityIndexWithEphemeralOffsets { + /// Reserves the next ephemeral offset for `factor_source_id` for `agnostic_path`. + /// Consecutive calls always returns a new value, which is `previous + 1` (given + /// the same `factor_source_id, agnostic_path`) + pub fn reserve( + &self, + factor_source_id: FactorSourceIDFromHash, + agnostic_path: IndexAgnosticPath, + ) -> HDPathValue { + let mut binding = self.ephemeral_offsets_per_factor_source.write().unwrap(); + if let Some(for_factor) = binding.get_mut(&factor_source_id) { + for_factor.reserve(agnostic_path) + } else { + let new = NextDerivationEntityIndexWithEphemeralOffsetsForFactorSource::default(); + let next = new.reserve(agnostic_path); + binding.insert(factor_source_id, new); + next + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + type Sut = NextDerivationEntityIndexWithEphemeralOffsets; + + #[test] + fn test_contiguous() { + let sut = Sut::default(); + let n = 4; + let indices = (0..n) + .map(|_| { + sut.reserve( + FactorSourceIDFromHash::fs0(), + DerivationPreset::AccountVeci + .index_agnostic_path_on_network(NetworkID::Mainnet), + ) + }) + .collect::>(); + assert_eq!(indices, IndexSet::::from_iter([0, 1, 2, 3])); + } + + #[test] + fn test_zero_for_each_factor_sources_first_time() { + let sut = Sut::default(); + let fsids = HDFactorSource::all() + .into_iter() + .map(|f| f.factor_source_id()) + .collect_vec(); + let indices = fsids + .clone() + .into_iter() + .map(|fsid| { + sut.reserve( + fsid, + DerivationPreset::AccountVeci + .index_agnostic_path_on_network(NetworkID::Mainnet), + ) + }) + .collect_vec(); + assert_eq!(indices, vec![0; fsids.len()]); + } + + #[test] + fn test_zero_for_each_derivation_preset() { + let sut = Sut::default(); + let derivation_presets = DerivationPreset::all(); + let indices = derivation_presets + .clone() + .into_iter() + .map(|preset| { + sut.reserve( + FactorSourceIDFromHash::fs0(), + preset.index_agnostic_path_on_network(NetworkID::Mainnet), + ) + }) + .collect_vec(); + assert_eq!(indices, vec![0; derivation_presets.len()]); + } + + #[test] + fn test_zero_for_each_network() { + let sut = Sut::default(); + let network_ids = NetworkID::all(); + let indices = network_ids + .clone() + .into_iter() + .map(|network_id| { + sut.reserve( + FactorSourceIDFromHash::fs0(), + DerivationPreset::AccountMfa.index_agnostic_path_on_network(network_id), + ) + }) + .collect_vec(); + assert_eq!(indices, vec![0; network_ids.len()]); + } +} diff --git a/src/factor_instances_provider/next_index_assigner/next_derivation_entity_index_with_ephemeral_offsets_for_factor_source.rs b/src/factor_instances_provider/next_index_assigner/next_derivation_entity_index_with_ephemeral_offsets_for_factor_source.rs new file mode 100644 index 00000000..ca0c1b8b --- /dev/null +++ b/src/factor_instances_provider/next_index_assigner/next_derivation_entity_index_with_ephemeral_offsets_for_factor_source.rs @@ -0,0 +1,28 @@ +use std::ops::{AddAssign, Index}; + +use crate::prelude::*; + +/// Ephemeral / "Local" offsets, is a collection of counters with offset added +/// on top of next index analysis based on cache or profile. This is used so that +/// the FactorInstanceProvider can consecutively call `next` N times to get a range of +/// of `N` unique indices, added to the otherwise next based on cache/profile analysis. +#[derive(Debug, Default)] +pub struct NextDerivationEntityIndexWithEphemeralOffsetsForFactorSource { + ephemeral_offsets: RwLock>, +} + +impl NextDerivationEntityIndexWithEphemeralOffsetsForFactorSource { + /// Returns the next free index for the IndexAgnosticPath, + /// and increases the local ephemeral offset. + pub fn reserve(&self, agnostic_path: IndexAgnosticPath) -> HDPathValue { + let mut binding = self.ephemeral_offsets.write().unwrap(); + if let Some(existing) = binding.get_mut(&agnostic_path) { + let free = *existing; + existing.add_assign(1); + free + } else { + binding.insert(agnostic_path, 1); + 0 + } + } +} diff --git a/src/factor_instances_provider/provider/factor_instances_cache.rs b/src/factor_instances_provider/provider/factor_instances_cache.rs new file mode 100644 index 00000000..834b03ff --- /dev/null +++ b/src/factor_instances_provider/provider/factor_instances_cache.rs @@ -0,0 +1,525 @@ +use std::{ + borrow::Borrow, + ops::{Add, Index}, +}; + +use crate::prelude::*; + +/// A cache of factor instances. +/// +/// Keyed under FactorSourceID and then under `IndexAgnosticPath`, each holding +/// an ordered set of Factor Instances, with contiguous derivation entity indices, +/// with lowest indices first in the set and highest last. +/// +/// Since an IndexAgnosticPath essentially is the tuple `(NetworkID, DerivationPreset)`, +/// you can think of the implementation to not be: +/// `IndexMap>` +/// but actually: +/// IndexMap>>`, +/// in fact it could be, but not sure it is more readable. But for the sake of visualizing +/// the cache we use that structure. +/// +/// E.g.: +/// ```ignore +/// [ +/// "FactorSourceID": [ +/// "Mainnet": [ +/// DerivationPreset::AccountVeci: [ +/// (0', key...), +/// (1', key...), +/// ... +/// (29', key...), +/// ], +/// DerivationPreset::AccountMfa: [ +/// (0^, key...), +/// (1^, key...), +/// ... +/// (29^, key...), +/// ], +/// DerivationPreset::IdentityVeci: [ +/// (0', key...), +/// ... +/// (29', key...), +/// ], +/// DerivationPreset::IdentityMfa: [ +/// (0^, key...), ..., (29^, key...), +/// ], +/// ], +/// "Stokenet": [ +/// DerivationPreset::AccountVeci: [ +/// (0', key...), ..., (29', key...), +/// ], +/// DerivationPreset::AccountMfa: [ +/// (0^, key...), ..., (29^, key...), +/// ], +/// DerivationPreset::IdentityVeci: [ +/// (0', key...), ... (29', key...), +/// ], +/// DerivationPreset::IdentityMfa: [ +/// (0^, key...), ..., (29^, key...), +/// ], +/// ], +/// ], +/// "FactorSourceID": [ +/// "Mainnet": [ +/// DerivationPreset::AccountVeci: [ +/// (0', key...), ..., (29', key...), +/// ], +/// DerivationPreset::AccountMfa: [ ... ], +/// DerivationPreset::IdentityVeci: [ ... ], +/// DerivationPreset::IdentityMfa: [ ... ], +/// ], +/// "Stokenet": [ +/// DerivationPreset::AccountVeci: [ +/// (0', key...), ..., (29', key...), +/// ], +/// DerivationPreset::AccountMfa: [ ... ], +/// DerivationPreset::IdentityVeci: [ ... ], +/// DerivationPreset::IdentityMfa: [ ... ], +/// ], +/// ], +/// ] +/// ``` +/// +/// This is the "in-memory" form of the cache. We would need to impl `Serde` for +/// it in Sargon. +/// +/// We use `IndexMap` instead of `HashMap` for future proofing when we serialize, +/// deserialize this cache, we want the JSON values to have stable ordering. Note +/// that the only truly **important** ordering is that of `FactorInstances` values, +/// which are ordered since it is a newtype around `IndexSet`. +/// +/// +/// The Serde impl of `IndexAgnosticPath` could be: +/// `"///"` as a string, e.g: +/// `"1/A/TX/U"`, where `U` is "Unsecurified" KeySpace. +/// Or if we don't wanna use such a "custom" one we can use `525`/`616` +/// discriminator for EntityKind and `1460`/`1678` for KeyKind: +/// "1/525/1460/U". +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct FactorInstancesCache { + /// PER FactorSource PER IndexAgnosticPath FactorInstances (matching that IndexAgnosticPath) + map: IndexMap>, +} + +impl FactorInstancesCache { + /// Inserts `instances` under `factor_source_id` by splitting them and grouping + /// them by their `IndexAgnosticPath`. + /// + /// Returns `Err` if any of the instances is in fact does NOT have `factor_source_id`, + /// as its factor source id. + /// + /// Returns `bool` indicating if an index was skipped resulting in non-contiguousness, which + /// we do not use for now. Might be something we enforce or not for certain operations + /// in the future. + pub fn insert_for_factor( + &mut self, + factor_source_id: &FactorSourceIDFromHash, + instances: &FactorInstances, + ) -> Result { + let mut skipped_an_index_resulting_in_non_contiguity = false; + + let instances_by_agnostic_path = InstancesByAgnosticPath::from(instances.clone()); + instances_by_agnostic_path.validate_from_source(factor_source_id)?; + if let Some(existing_for_factor) = self.map.get_mut(factor_source_id) { + for (agnostic_path, instances) in instances_by_agnostic_path { + let instances = instances.factor_instances(); + + if let Some(existing_for_path) = existing_for_factor.get_mut(&agnostic_path) { + if let Some(fi) = instances + .intersection(&existing_for_path.factor_instances()) + .next() + { + return Err(CommonError::CacheAlreadyContainsFactorInstance { + derivation_path: fi.derivation_path(), + }); + } + + if let Some(last) = existing_for_path.factor_instances().last() { + if instances.first().unwrap().derivation_entity_base_index() + != last.derivation_entity_base_index() + 1 + { + warn!( + "Non-contiguous indices, the index `{}` was skipped!", + last.derivation_entity_base_index() + 1 + ); + skipped_an_index_resulting_in_non_contiguity = true; + } + } + existing_for_path.extend(instances); + } else { + existing_for_factor.insert(agnostic_path, FactorInstances::from(instances)); + } + } + } else { + self.map + .insert(*factor_source_id, instances_by_agnostic_path.0); + } + + Ok(skipped_an_index_resulting_in_non_contiguity) + } + + /// Inserts all instance in `per_factor`. + pub fn insert_all( + &mut self, + per_factor: &IndexMap, + ) -> Result<()> { + for (factor_source_id, instances) in per_factor { + _ = self.insert_for_factor(factor_source_id, instances)?; + } + Ok(()) + } + + /// Returns the MAX derivation entity index for the given `factor_source_id` and `index_agnostic_path`. + pub fn max_index_for( + &self, + factor_source_id: impl Borrow, + index_agnostic_path: impl Borrow, + ) -> Option { + self.get_mono_factor(factor_source_id, index_agnostic_path) + .unwrap_or_default() + .factor_instances() + .into_iter() + .map(|fi| fi.derivation_entity_index()) + .max() + } + + /// Returns enough instances to satisfy the requested quantity for each factor source, + /// **OR LESS**, never more, and if less, it means we MUST derive more, and if we + /// must derive more, this function returns the quantities to derive for each factor source, + /// for each derivation preset, not only the originally requested one. + pub fn get_poly_factor_with_quantities( + &self, + factor_source_ids: &IndexSet, + originally_requested_quantified_derivation_preset: &QuantifiedDerivationPreset, + network_id: NetworkID, + ) -> Result { + let target_quantity = originally_requested_quantified_derivation_preset.quantity; + let mut pf_instances = IndexMap::::new(); + let mut pf_pdp_qty_to_derive = + IndexMap::>::new(); + let mut is_quantity_satisfied_for_all_factor_sources = true; + + for factor_source_id in factor_source_ids { + for preset in DerivationPreset::all() { + let index_agnostic_path = preset.index_agnostic_path_on_network(network_id); + let for_preset = self + .get_mono_factor(factor_source_id, index_agnostic_path) + .unwrap_or_default(); + let count_in_cache = for_preset.len(); + if preset == originally_requested_quantified_derivation_preset.derivation_preset { + let satisfies_requested_quantity = count_in_cache >= target_quantity; + if satisfies_requested_quantity { + // The instances in the cache can satisfy the requested quantity + // for this factor source for this derivation preset + pf_instances.append_or_insert_to( + factor_source_id, + // Only take the first `target_quantity` instances + // to be used, the rest are not needed and should + // remain in the cache (later we will call delete on + // all those instances.) + for_preset.split_at(target_quantity).0, + ); + } else { + // The instances in the cache cannot satisfy the requested quantity + // we must derive more! + is_quantity_satisfied_for_all_factor_sources = false; + // Since we are deriving more we might as well ensure that the + // cache is filled with `CACHE_FILLING_QUANTITY` **AFTER** the + // requested quantity is satisfied, meaning we will not only + // derive `CACHE_FILLING_QUANTITY - count_in_cache`, instead we + // derive the `target_quantity` as well. + let quantity_to_derive = + CACHE_FILLING_QUANTITY - count_in_cache + target_quantity; + pf_pdp_qty_to_derive.append_or_insert_element_to( + factor_source_id, + (preset, quantity_to_derive), + ); + // insert all instances to be used directly + pf_instances.append_or_insert_to(factor_source_id, for_preset.clone()); + } + } else { + // Not originally requested derivation preset, calculate number + // of instances to derive IF we are going to derive anyway, + // we wanna FILL the cache for those derivation presets as well. + if count_in_cache < CACHE_FILLING_QUANTITY { + let qty_to_derive = CACHE_FILLING_QUANTITY - count_in_cache; + pf_pdp_qty_to_derive + .append_or_insert_element_to(factor_source_id, (preset, qty_to_derive)); + } + } + } + } + let outcome = if is_quantity_satisfied_for_all_factor_sources { + CachedInstancesWithQuantitiesOutcome::Satisfied(pf_instances) + } else { + CachedInstancesWithQuantitiesOutcome::NotSatisfied { + partial_instances: pf_instances, + quantities_to_derive: pf_pdp_qty_to_derive, + } + }; + Ok(outcome) + } +} + +#[derive(enum_as_inner::EnumAsInner)] +pub enum CachedInstancesWithQuantitiesOutcome { + Satisfied(IndexMap), + NotSatisfied { + partial_instances: IndexMap, + quantities_to_derive: IndexMap>, + }, +} + +impl FactorInstancesCache { + pub fn get_mono_factor( + &self, + factor_source_id: impl Borrow, + index_agnostic_path: impl Borrow, + ) -> Option { + let for_factor = self.map.get(factor_source_id.borrow())?; + let instances = for_factor.get(index_agnostic_path.borrow())?; + Some(instances.clone()) + } + + pub fn delete(&mut self, pf_instances: &IndexMap) { + for (factor_source_id, instances_to_delete) in pf_instances { + if instances_to_delete.is_empty() { + continue; + } + let existing_for_factor = self + .map + .get_mut(factor_source_id) + .expect("expected to delete factors"); + + let instances_to_delete_by_path = + InstancesByAgnosticPath::from(instances_to_delete.clone()); + for (index_agnostic_path, instances_to_delete) in instances_to_delete_by_path { + let instances_to_delete = + IndexSet::::from_iter( + instances_to_delete.into_iter(), + ); + + let existing_for_path = existing_for_factor + .get(&index_agnostic_path) + .expect("expected to delete") + .factor_instances(); + + if !existing_for_path.is_superset(&instances_to_delete) { + panic!("Programmer error! Some of the factors to delete were not in cache!"); + } + let to_keep = existing_for_path + .symmetric_difference(&instances_to_delete) + .cloned() + .collect::(); + + // replace + existing_for_factor.insert(index_agnostic_path, to_keep); + } + } + + self.prune(); + } + + /// "Prunes" the cache from empty collections + fn prune(&mut self) { + let ids = self.factor_source_ids(); + for factor_source_id in ids.iter() { + let inner_map = self.map.get_mut(factor_source_id).unwrap(); + if inner_map.is_empty() { + // empty map, prune it! + self.map.shift_remove(factor_source_id); + continue; + } + // see if pruning of instances inside of values `inner_map` is needed + let inner_ids = inner_map + .keys() + .cloned() + .collect::>(); + for inner_id in inner_ids.iter() { + if inner_map.get(inner_id).unwrap().is_empty() { + // FactorInstances empty, prune it! + inner_map.shift_remove(inner_id); + } + } + } + } + + fn factor_source_ids(&self) -> IndexSet { + self.map.keys().cloned().collect() + } + pub fn insert(&mut self, pf_instances: &IndexMap) { + self.insert_all(pf_instances).expect("works") + } + + /// Reads out the instance of `factor_source_id` without mutating the cache. + pub fn peek_all_instances_of_factor_source( + &self, + factor_source_id: FactorSourceIDFromHash, + ) -> Option> { + self.map.get(&factor_source_id).cloned() + } + + pub fn total_number_of_factor_instances(&self) -> usize { + self.map + .values() + .map(|x| { + x.values() + .map(|y| y.len()) + .reduce(Add::add) + .unwrap_or_default() + }) + .reduce(Add::add) + .unwrap_or_default() + } +} + +#[cfg(test)] +impl FactorInstancesCache { + /// Queries the cache to see if the cache is full for factor_source_id for + /// each DerivationPreset + pub fn is_full(&self, network_id: NetworkID, factor_source_id: FactorSourceIDFromHash) -> bool { + DerivationPreset::all() + .into_iter() + .map(|preset| { + self.get_poly_factor_with_quantities( + &IndexSet::just(factor_source_id), + &QuantifiedDerivationPreset::new(preset, CACHE_FILLING_QUANTITY), + network_id, + ) + }) + .all(|outcome| { + matches!( + outcome, + Ok(CachedInstancesWithQuantitiesOutcome::Satisfied(_)) + ) + }) + } + + pub fn assert_is_full(&self, network_id: NetworkID, factor_source_id: FactorSourceIDFromHash) { + assert!(self.is_full(network_id, factor_source_id)); + } +} + +#[cfg(test)] +mod tests { + + use std::fs; + + use super::*; + + type Sut = FactorInstancesCache; + + #[test] + fn non_contiguous_indices() { + let mut sut = Sut::default(); + let fsid = FactorSourceIDFromHash::fs0(); + let fi0 = HierarchicalDeterministicFactorInstance::mainnet_tx( + CAP26EntityKind::Account, + HDPathComponent::unsecurified_hardening_base_index(0), + fsid, + ); + assert!(!sut + .insert_for_factor(&fsid, &FactorInstances::from_iter([fi0])) + .unwrap()); + let fi2 = HierarchicalDeterministicFactorInstance::mainnet_tx( + CAP26EntityKind::Account, + HDPathComponent::unsecurified_hardening_base_index(2), // OH NO! Skipping `1` + fsid, + ); + assert!(sut + .insert_for_factor(&fsid, &FactorInstances::from_iter([fi2])) + .unwrap(),); + } + + #[test] + fn factor_source_discrepancy() { + let mut sut = Sut::default(); + let fi0 = HierarchicalDeterministicFactorInstance::mainnet_tx( + CAP26EntityKind::Account, + HDPathComponent::unsecurified_hardening_base_index(0), + FactorSourceIDFromHash::fs0(), + ); + assert!(sut + .insert_for_factor( + &FactorSourceIDFromHash::fs1(), + &FactorInstances::from_iter([fi0]) + ) + .is_err()); + } + + #[test] + fn delete() { + let mut sut = Sut::default(); + + let factor_source_ids = HDFactorSource::all() + .into_iter() + .map(|f| f.factor_source_id()) + .collect::>(); + + let n = 30; + let mut to_delete = IndexMap::::new(); + let mut to_remain = IndexMap::::new(); + for factor_source_id in factor_source_ids.clone() { + let fsid = factor_source_id; + let instances = (0..n) + .map(|i| { + let fi = HierarchicalDeterministicFactorInstance::mainnet_tx( + CAP26EntityKind::Account, + HDPathComponent::unsecurified_hardening_base_index(i), + fsid, + ); + if i < 10 { + to_delete.append_or_insert_to(&fsid, IndexSet::just(fi.clone())); + } else { + to_remain.append_or_insert_to(&fsid, IndexSet::just(fi.clone())); + } + fi + }) + .collect::>(); + + sut.insert_for_factor(&fsid, &FactorInstances::from(instances)) + .unwrap(); + } + + sut.delete(&to_delete); + + let path = &IndexAgnosticPath::new( + NetworkID::Mainnet, + CAP26EntityKind::Account, + CAP26KeyKind::TransactionSigning, + KeySpace::Unsecurified, + ); + for (f, instances) in to_remain { + assert_eq!(sut.get_mono_factor(f, path).unwrap(), instances) + } + } + + #[test] + fn throws_if_same_is_added() { + let mut sut = Sut::default(); + let fsid = FactorSourceIDFromHash::fs0(); + let fi0 = HierarchicalDeterministicFactorInstance::mainnet_tx( + CAP26EntityKind::Account, + HDPathComponent::unsecurified_hardening_base_index(0), + fsid, + ); + let fi1 = HierarchicalDeterministicFactorInstance::mainnet_tx( + CAP26EntityKind::Account, + HDPathComponent::unsecurified_hardening_base_index(1), + fsid, + ); + assert!(!sut + .insert_for_factor(&fsid, &FactorInstances::from_iter([fi0.clone(), fi1])) + .unwrap()); + + assert_eq!( + sut.insert_for_factor(&fsid, &FactorInstances::from_iter([fi0.clone()])) + .err() + .unwrap(), + CommonError::CacheAlreadyContainsFactorInstance { + derivation_path: fi0.derivation_path() + } + ); + } +} diff --git a/src/factor_instances_provider/provider/factor_instances_provider.rs b/src/factor_instances_provider/provider/factor_instances_provider.rs new file mode 100644 index 00000000..21cea2ba --- /dev/null +++ b/src/factor_instances_provider/provider/factor_instances_provider.rs @@ -0,0 +1,321 @@ +use std::sync::{Arc, RwLock}; + +use itertools::cloned; + +use crate::{factor_instances_provider::next_index_assigner, prelude::*}; + +/// A coordinator between a cache, an optional profile and the KeysCollector. +/// +/// We can ask this type to provide FactorInstances for some operation, either +/// creation of new virtual accounts or securifying accounts (or analogously for identities). +/// It will try to read instances from the cache, if any, and if there are not enough instances +/// in the cache, it will derive more instances and save them into the cache. +/// +/// We are always reading from the beginning of each FactorInstance collection in the cache, +/// and we are always appending to the end. +/// +/// Whenever we need to derive more, we always derive for all `IndexAgnosticPath` "presets", +/// i.e. we are not only filling the cache with factor instances relevant to the operation +/// but rather we are filling the cache with factor instances for all kinds of operations, i.e. +/// if we did not have `CACHE_FILLING_QUANTITY` instances for "account_mfa", when we tried +/// to read "account_veci" instances, we will derive more "account_mfa" instances as well, +/// so many that at the end of execution we will have `CACHE_FILLING_QUANTITY` instances for +/// both "account_veci" and "account_mfa" (and same for identities). +pub struct FactorInstancesProvider<'a> { + network_id: NetworkID, + factor_sources: IndexSet, + profile: Option, + cache: &'a mut FactorInstancesCache, + interactors: Arc, +} + +/// =============== +/// PUBLIC +/// =============== +impl<'a> FactorInstancesProvider<'a> { + pub fn new( + network_id: NetworkID, + factor_sources: IndexSet, + profile: impl Into>, + cache: &'a mut FactorInstancesCache, + interactors: Arc, + ) -> Self { + Self { + network_id, + factor_sources, + profile: profile.into(), + cache, + interactors, + } + } + + pub async fn provide( + self, + quantified_derivation_preset: QuantifiedDerivationPreset, + ) -> Result { + let mut _self = self; + + _self._provide(quantified_derivation_preset).await + } +} + +/// Uses a `FactorInstancesProvider` to fill the cache with instances for a new FactorSource. +pub struct CacheFiller; +impl CacheFiller { + /// Uses a `FactorInstancesProvider` to fill the `cache` with FactorInstances for a new FactorSource. + /// Saves FactorInstances into the mutable `cache` parameter and returns a + /// copy of the instances. + pub async fn for_new_factor_source( + cache: &mut FactorInstancesCache, + profile: Option, + factor_source: HDFactorSource, + network_id: NetworkID, // typically mainnet + interactors: Arc, + ) -> Result { + let provider = FactorInstancesProvider::new( + network_id, + IndexSet::just(factor_source.clone()), + profile, + cache, + interactors, + ); + let quantities = IndexMap::kv( + factor_source.factor_source_id(), + DerivationPreset::all() + .into_iter() + .map(|dp| (dp, CACHE_FILLING_QUANTITY)) + .collect::>(), + ); + let derived = provider.derive_more(quantities).await?; + + cache.insert(&derived); + + let derived = derived + .get(&factor_source.factor_source_id()) + .unwrap() + .clone(); + let outcome = InternalFactorInstancesProviderOutcomeForFactor::new( + factor_source.factor_source_id(), + derived.clone(), + FactorInstances::default(), + FactorInstances::default(), + derived, + ); + Ok(outcome.into()) + } +} + +/// =============== +/// Private +/// =============== +impl<'a> FactorInstancesProvider<'a> { + async fn _provide( + &mut self, + quantified_derivation_preset: QuantifiedDerivationPreset, + ) -> Result { + let factor_sources = self.factor_sources.clone(); + let network_id = self.network_id; + let cached = self.cache.get_poly_factor_with_quantities( + &factor_sources + .iter() + .map(|f| f.factor_source_id()) + .collect(), + &quantified_derivation_preset, + network_id, + )?; + + match cached { + CachedInstancesWithQuantitiesOutcome::Satisfied(enough_instances) => { + // Remove the instances which are going to be used from the cache + // since we only peeked at them. + self.cache.delete(&enough_instances); + Ok(InternalFactorInstancesProviderOutcome::satisfied_by_cache( + enough_instances, + )) + } + CachedInstancesWithQuantitiesOutcome::NotSatisfied { + quantities_to_derive, + partial_instances, + } => { + self.derive_more_and_cache( + quantified_derivation_preset, + partial_instances, + quantities_to_derive, + ) + .await + } + } + } + + async fn derive_more_and_cache( + &mut self, + quantified_derivation_preset: QuantifiedDerivationPreset, + pf_found_in_cache_leq_requested: IndexMap, + pf_pdp_qty_to_derive: IndexMap>, + ) -> Result { + let pf_newly_derived = self.derive_more(pf_pdp_qty_to_derive).await?; + + let Split { + pf_to_use_directly, + pf_to_cache, + } = self.split( + &quantified_derivation_preset, + &pf_found_in_cache_leq_requested, + &pf_newly_derived, + ); + + self.cache.delete(&pf_found_in_cache_leq_requested); + self.cache.insert(&pf_to_cache); + + let outcome = InternalFactorInstancesProviderOutcome::transpose( + pf_to_cache, + pf_to_use_directly, + pf_found_in_cache_leq_requested, + pf_newly_derived, + ); + let outcome = outcome; + Ok(outcome) + } + + /// Per factor, split the instances into those to use directly and those to cache. + /// based on the originally requested quantity. + fn split( + &self, + originally_requested_quantified_derivation_preset: &QuantifiedDerivationPreset, + pf_found_in_cache_leq_requested: &IndexMap, + pf_newly_derived: &IndexMap, + ) -> Split { + // Start by merging the instances found in cache and the newly derived instances, + // into a single collection of instances per factor source, with the + // instances from cache first in the list (per factor), and then the newly derived. + // this is important so that we consume the instances from cache first. + let pf_derived_appended_to_from_cache = self + .factor_sources + .clone() + .into_iter() + .map(|f| f.factor_source_id()) + .map(|factor_source_id| { + let mut merged = IndexSet::new(); + let from_cache = pf_found_in_cache_leq_requested + .get(&factor_source_id) + .cloned() + .unwrap_or_default(); + let newly_derived = pf_newly_derived + .get(&factor_source_id) + .cloned() + .unwrap_or_default(); + // IMPORTANT: Must put instances from cache **first**... + merged.extend(from_cache); + // ... and THEN the newly derived, so we consume the ones with + // lower index from cache first. + merged.extend(newly_derived); + + (factor_source_id, FactorInstances::from(merged)) + }) + .collect::>(); + + let mut pf_to_use_directly = IndexMap::new(); + let mut pf_to_cache = IndexMap::::new(); + let quantity_originally_requested = + originally_requested_quantified_derivation_preset.quantity; + let preset_originally_requested = + originally_requested_quantified_derivation_preset.derivation_preset; + + // Using the merged map, split the instances into those to use directly and those to cache. + for (factor_source_id, instances) in pf_derived_appended_to_from_cache.clone().into_iter() { + let mut instances_by_derivation_preset = InstancesByDerivationPreset::from(instances); + + if let Some(instances_relevant_to_use_directly_with_abundance) = + instances_by_derivation_preset.remove(preset_originally_requested) + { + let (to_use_directly, to_cache) = instances_relevant_to_use_directly_with_abundance + .split_at(quantity_originally_requested); + pf_to_use_directly.insert(factor_source_id, to_use_directly); + pf_to_cache.insert(factor_source_id, to_cache); + } + + pf_to_cache.append_or_insert_to( + factor_source_id, + instances_by_derivation_preset.all_instances(), + ); + } + + Split { + pf_to_use_directly, + pf_to_cache, + } + } + + async fn derive_more( + &self, + pf_pdp_quantity_to_derive: IndexMap< + FactorSourceIDFromHash, + IndexMap, + >, + ) -> Result> { + let factor_sources = self.factor_sources.clone(); + let network_id = self.network_id; + + let next_index_assigner = NextDerivationEntityIndexAssigner::new( + network_id, + self.profile.clone(), + self.cache.clone(), + ); + + let pf_paths = pf_pdp_quantity_to_derive + .into_iter() + .map(|(factor_source_id, pdp_quantity_to_derive)| { + let paths = pdp_quantity_to_derive + .into_iter() + .map(|(derivation_preset, qty)| { + // `qty` many paths + let paths = (0..qty) + .map(|_| { + let index_agnostic_path = + derivation_preset.index_agnostic_path_on_network(network_id); + let index = next_index_assigner + .next(factor_source_id, index_agnostic_path)?; + Ok(DerivationPath::from((index_agnostic_path, index))) + }) + .collect::>>()?; + + Ok(paths) + }) + .collect::>>>()?; + + // flatten (I was unable to use `flat_map` above combined with `Result`...) + let paths = paths.into_iter().flatten().collect::>(); + + Ok((factor_source_id, paths)) + }) + .collect::>>>()?; + + let keys_collector = + KeysCollector::new(factor_sources, pf_paths.clone(), self.interactors.clone())?; + + let pf_derived = keys_collector.collect_keys().await.factors_by_source; + + let mut pf_instances = IndexMap::::new(); + + for (factor_source_id, paths) in pf_paths { + let derived_for_factor = pf_derived + .get(&factor_source_id) + .cloned() + .unwrap_or_default(); // if None -> Empty -> fail below. + if derived_for_factor.len() < paths.len() { + return Err(CommonError::FactorInstancesProviderDidNotDeriveEnoughFactors); + } + pf_instances.insert( + factor_source_id, + derived_for_factor.into_iter().collect::(), + ); + } + + Ok(pf_instances) + } +} + +struct Split { + pf_to_use_directly: IndexMap, + pf_to_cache: IndexMap, +} diff --git a/src/factor_instances_provider/provider/factor_instances_provider_unit_tests.rs b/src/factor_instances_provider/provider/factor_instances_provider_unit_tests.rs new file mode 100644 index 00000000..31625fcf --- /dev/null +++ b/src/factor_instances_provider/provider/factor_instances_provider_unit_tests.rs @@ -0,0 +1,1644 @@ +#![cfg(test)] + +use std::ops::{Add, AddAssign}; + +use crate::{factor_instances_provider::provider::test_sargon_os::SargonOS, prelude::*}; + +#[actix_rt::test] +async fn create_accounts_when_last_is_used_cache_is_fill_only_with_account_vecis_and_if_profile_is_used_a_new_account_is_created( +) { + let (mut os, bdfs) = SargonOS::with_bdfs().await; + for i in 0..CACHE_FILLING_QUANTITY { + let name = format!("Acco {}", i); + let (acco, stats) = os + .new_mainnet_account_with_bdfs(name.clone()) + .await + .unwrap(); + assert_eq!(acco.name, name); + assert_eq!(stats.debug_was_cached.len(), 0); + assert_eq!(stats.debug_was_derived.len(), 0); + } + assert_eq!( + os.profile_snapshot().get_accounts().len(), + CACHE_FILLING_QUANTITY + ); + + let (acco, stats) = os + .new_mainnet_account_with_bdfs("newly derive") + .await + .unwrap(); + + assert_eq!( + os.profile_snapshot().get_accounts().len(), + CACHE_FILLING_QUANTITY + 1 + ); + + assert_eq!(stats.debug_was_cached.len(), CACHE_FILLING_QUANTITY); + assert_eq!(stats.debug_was_derived.len(), CACHE_FILLING_QUANTITY + 1); + + assert_eq!( + acco.as_unsecurified() + .unwrap() + .factor_instance() + .derivation_entity_index(), + HDPathComponent::unsecurified_hardening_base_index(30) + ); + assert!(os + .cache_snapshot() + .is_full(NetworkID::Mainnet, bdfs.factor_source_id())); + + // and another one + let (acco, stats) = os + .new_mainnet_account_with_bdfs("newly derive 2") + .await + .unwrap(); + + assert_eq!( + os.profile_snapshot().get_accounts().len(), + CACHE_FILLING_QUANTITY + 2 + ); + + assert_eq!(stats.debug_was_cached.len(), 0); + assert_eq!(stats.debug_was_derived.len(), 0); + + assert_eq!( + acco.as_unsecurified() + .unwrap() + .factor_instance() + .derivation_entity_index(), + HDPathComponent::unsecurified_hardening_base_index(31) + ); + assert!( + !os.cache_snapshot() + .is_full(NetworkID::Mainnet, bdfs.factor_source_id()), + "just consumed one, so not full" + ); +} + +#[actix_rt::test] +async fn add_factor_source() { + let mut os = SargonOS::new(); + assert_eq!(os.cache_snapshot().total_number_of_factor_instances(), 0); + assert_eq!(os.profile_snapshot().factor_sources.len(), 0); + let factor_source = HDFactorSource::sample(); + os.add_factor_source(factor_source.clone()).await.unwrap(); + assert!( + os.cache_snapshot() + .is_full(NetworkID::Mainnet, factor_source.factor_source_id()), + "Should have put factors into the cache." + ); + assert_eq!( + os.profile_snapshot().factor_sources, + IndexSet::just(factor_source) + ); +} + +#[actix_rt::test] +async fn adding_accounts_and_clearing_cache_in_between() { + let (mut os, _) = SargonOS::with_bdfs().await; + assert!(os.profile_snapshot().get_accounts().is_empty()); + let (alice, stats) = os.new_mainnet_account_with_bdfs("alice").await.unwrap(); + assert!(!stats.debug_found_in_cache.is_empty()); + assert!(stats.debug_was_cached.is_empty()); + assert!(stats.debug_was_derived.is_empty()); + os.clear_cache(); + + let (bob, stats) = os.new_mainnet_account_with_bdfs("bob").await.unwrap(); + assert!(stats.debug_found_in_cache.is_empty()); + assert!(!stats.debug_was_cached.is_empty()); + assert!(!stats.debug_was_derived.is_empty()); + assert_ne!(alice, bob); + + assert_eq!(os.profile_snapshot().get_accounts().len(), 2); +} + +#[actix_rt::test] +async fn adding_personas_and_clearing_cache_in_between() { + let (mut os, _) = SargonOS::with_bdfs().await; + assert!(os.profile_snapshot().get_personas().is_empty()); + let (batman, stats) = os.new_mainnet_persona_with_bdfs("Batman").await.unwrap(); + + assert_eq!( + batman + .clone() + .security_state + .into_unsecured() + .unwrap() + .derivation_path() + .entity_kind, + CAP26EntityKind::Identity + ); + assert!(!stats.debug_found_in_cache.is_empty()); + assert!(stats.debug_was_cached.is_empty()); + assert!(stats.debug_was_derived.is_empty()); + os.clear_cache(); + + let (satoshi, stats) = os.new_mainnet_persona_with_bdfs("Satoshi").await.unwrap(); + assert!(stats.debug_found_in_cache.is_empty()); + assert!(!stats.debug_was_cached.is_empty()); + assert!(!stats.debug_was_derived.is_empty()); + assert_ne!(batman, satoshi); + + assert_eq!(os.profile_snapshot().get_personas().len(), 2); +} + +#[actix_rt::test] +async fn add_account_and_personas_mixed() { + let (mut os, _) = SargonOS::with_bdfs().await; + assert!(os.profile_snapshot().get_personas().is_empty()); + assert!(os.profile_snapshot().get_accounts().is_empty()); + + let (batman, stats) = os.new_mainnet_persona_with_bdfs("Batman").await.unwrap(); + assert!(stats.debug_was_derived.is_empty()); + + let (alice, stats) = os.new_mainnet_account_with_bdfs("alice").await.unwrap(); + assert!(stats.debug_was_derived.is_empty()); + + let (satoshi, stats) = os.new_mainnet_persona_with_bdfs("Satoshi").await.unwrap(); + assert!(stats.debug_was_derived.is_empty()); + + assert_ne!(batman.entity_address(), satoshi.entity_address()); + + let (bob, stats) = os.new_mainnet_account_with_bdfs("bob").await.unwrap(); + assert!(stats.debug_was_derived.is_empty()); + assert_ne!(alice.entity_address(), bob.entity_address()); + + assert_eq!(os.profile_snapshot().get_personas().len(), 2); + assert_eq!(os.profile_snapshot().get_accounts().len(), 2); +} + +#[actix_rt::test] +async fn adding_accounts_different_networks_different_factor_sources() { + let mut os = SargonOS::new(); + assert_eq!(os.cache_snapshot().total_number_of_factor_instances(), 0); + + let fs_device = HDFactorSource::device(); + let fs_arculus = HDFactorSource::arculus(); + let fs_ledger = HDFactorSource::ledger(); + + os.add_factor_source(fs_device.clone()).await.unwrap(); + os.add_factor_source(fs_arculus.clone()).await.unwrap(); + os.add_factor_source(fs_ledger.clone()).await.unwrap(); + + assert_eq!( + os.cache_snapshot().total_number_of_factor_instances(), + 3 * 4 * CACHE_FILLING_QUANTITY + ); + + assert!(os.profile_snapshot().get_accounts().is_empty()); + assert_eq!(os.profile_snapshot().factor_sources.len(), 3); + + let (alice, stats) = os + .new_account(fs_device.clone(), NetworkID::Mainnet, "Alice") + .await + .unwrap(); + assert!(stats.debug_was_derived.is_empty()); + + let (bob, stats) = os + .new_account(fs_device.clone(), NetworkID::Mainnet, "Bob") + .await + .unwrap(); + assert!(stats.debug_was_derived.is_empty()); + + let (carol, stats) = os + .new_account(fs_device.clone(), NetworkID::Stokenet, "Carol") + .await + .unwrap(); + assert!( + !stats.debug_was_derived.is_empty(), + "Should have derived more, since first time Stokenet is used!" + ); + + let (diana, stats) = os + .new_account(fs_device.clone(), NetworkID::Stokenet, "Diana") + .await + .unwrap(); + assert!(stats.debug_was_derived.is_empty()); + + let (erin, stats) = os + .new_account(fs_arculus.clone(), NetworkID::Mainnet, "Erin") + .await + .unwrap(); + assert!(stats.debug_was_derived.is_empty()); + + let (frank, stats) = os + .new_account(fs_arculus.clone(), NetworkID::Mainnet, "Frank") + .await + .unwrap(); + assert!(stats.debug_was_derived.is_empty()); + + let (grace, stats) = os + .new_account(fs_arculus.clone(), NetworkID::Stokenet, "Grace") + .await + .unwrap(); + assert!( + !stats.debug_was_derived.is_empty(), + "Should have derived more, since first time Stokenet is used with the Arculus!" + ); + + let (helena, stats) = os + .new_account(fs_arculus.clone(), NetworkID::Stokenet, "Helena") + .await + .unwrap(); + assert!(stats.debug_was_derived.is_empty()); + + let (isabel, stats) = os + .new_account(fs_ledger.clone(), NetworkID::Mainnet, "isabel") + .await + .unwrap(); + assert!(stats.debug_was_derived.is_empty()); + + let (jenny, stats) = os + .new_account(fs_ledger.clone(), NetworkID::Mainnet, "Jenny") + .await + .unwrap(); + assert!(stats.debug_was_derived.is_empty()); + + let (klara, stats) = os + .new_account(fs_ledger.clone(), NetworkID::Stokenet, "Klara") + .await + .unwrap(); + assert!( + !stats.debug_was_derived.is_empty(), + "Should have derived more, since first time Stokenet is used with the Ledger!" + ); + + let (lisa, stats) = os + .new_account(fs_ledger.clone(), NetworkID::Stokenet, "Lisa") + .await + .unwrap(); + assert!(stats.debug_was_derived.is_empty()); + + assert_eq!(os.profile_snapshot().get_accounts().len(), 12); + + let accounts = vec![ + alice, bob, carol, diana, erin, frank, grace, helena, isabel, jenny, klara, lisa, + ]; + + let factor_source_count = os.profile_snapshot().factor_sources.len(); + let network_count = os.profile_snapshot().networks.len(); + assert_eq!( + os.cache_snapshot().total_number_of_factor_instances(), + network_count + * factor_source_count + * DerivationPreset::all().len() + * CACHE_FILLING_QUANTITY + - accounts.len() + + factor_source_count // we do `+ factor_source_count` since every time a factor source is used on a new network for the first time, we derive `CACHE_FILLING_QUANTITY + 1` + ); + + assert_eq!( + os.profile_snapshot() + .get_accounts() + .into_iter() + .map(|a| a.entity_address()) + .collect::>(), + accounts + .into_iter() + .map(|a| a.entity_address()) + .collect::>() + ); +} + +#[actix_rt::test] +async fn test_securified_accounts() { + let (mut os, bdfs) = SargonOS::with_bdfs().await; + let alice = os + .new_account_with_bdfs(NetworkID::Mainnet, "Alice") + .await + .unwrap() + .0; + + let bob = os + .new_account_with_bdfs(NetworkID::Mainnet, "Bob") + .await + .unwrap() + .0; + assert_ne!(alice.address(), bob.address()); + let ledger = HDFactorSource::ledger(); + let arculus = HDFactorSource::arculus(); + let yubikey = HDFactorSource::yubikey(); + os.add_factor_source(ledger.clone()).await.unwrap(); + os.add_factor_source(arculus.clone()).await.unwrap(); + os.add_factor_source(yubikey.clone()).await.unwrap(); + let shield_0 = + MatrixOfFactorSources::new([bdfs.clone(), ledger.clone(), arculus.clone()], 2, []); + + let (securified_accounts, stats) = os + .securify_accounts( + IndexSet::from_iter([alice.entity_address(), bob.entity_address()]), + shield_0, + ) + .await + .unwrap(); + + assert!( + !stats.derived_any_new_instance_for_any_factor_source(), + "should have used cache" + ); + + let alice_sec = securified_accounts + .clone() + .into_iter() + .find(|x| x.address() == alice.entity_address()) + .unwrap(); + + assert_eq!( + alice_sec.securified_entity_control().veci.unwrap().clone(), + alice.as_unsecurified().unwrap().veci() + ); + let alice_matrix = alice_sec.securified_entity_control().matrix.clone(); + assert_eq!(alice_matrix.threshold, 2); + + assert_eq!( + alice_matrix + .all_factors() + .into_iter() + .map(|f| f.factor_source_id()) + .collect_vec(), + [ + bdfs.factor_source_id(), + ledger.factor_source_id(), + arculus.factor_source_id() + ] + ); + + assert_eq!( + alice_matrix + .all_factors() + .into_iter() + .map(|f| f.derivation_entity_index()) + .collect_vec(), + [ + HDPathComponent::securifying_base_index(0), + HDPathComponent::securifying_base_index(0), + HDPathComponent::securifying_base_index(0) + ] + ); + + // assert bob + + let bob_sec = securified_accounts + .clone() + .into_iter() + .find(|x| x.address() == bob.entity_address()) + .unwrap(); + + assert_eq!( + bob_sec.securified_entity_control().veci.unwrap().clone(), + bob.as_unsecurified().unwrap().veci() + ); + let bob_matrix = bob_sec.securified_entity_control().matrix.clone(); + assert_eq!(bob_matrix.threshold, 2); + + assert_eq!( + bob_matrix + .all_factors() + .into_iter() + .map(|f| f.factor_source_id()) + .collect_vec(), + [ + bdfs.factor_source_id(), + ledger.factor_source_id(), + arculus.factor_source_id() + ] + ); + + assert_eq!( + bob_matrix + .all_factors() + .into_iter() + .map(|f| f.derivation_entity_index()) + .collect_vec(), + [ + HDPathComponent::securifying_base_index(1), + HDPathComponent::securifying_base_index(1), + HDPathComponent::securifying_base_index(1) + ] + ); + + let carol = os + .new_account(ledger.clone(), NetworkID::Mainnet, "Carol") + .await + .unwrap() + .0; + + assert_eq!( + carol + .as_unsecurified() + .unwrap() + .veci() + .factor_instance() + .derivation_entity_index() + .base_index(), + 0, + "First account created with ledger, should have index 0, even though this ledger was used in the shield, since we are using two different KeySpaces for Securified and Unsecurified accounts." + ); + + let (securified_accounts, stats) = os + .securify_accounts( + IndexSet::just(carol.entity_address()), + MatrixOfFactorSources::new([], 0, [yubikey.clone()]), + ) + .await + .unwrap(); + assert!( + !stats.derived_any_new_instance_for_any_factor_source(), + "should have used cache" + ); + let carol_sec = securified_accounts + .clone() + .into_iter() + .find(|x| x.address() == carol.entity_address()) + .unwrap(); + + let carol_matrix = carol_sec.securified_entity_control().matrix.clone(); + + assert_eq!( + carol_matrix + .all_factors() + .into_iter() + .map(|f| f.factor_source_id()) + .collect_vec(), + [yubikey.factor_source_id()] + ); + + assert_eq!( + carol_matrix + .all_factors() + .into_iter() + .map(|f| f.derivation_entity_index()) + .collect_vec(), + [HDPathComponent::securifying_base_index(0)] + ); + + // Update Alice's shield to only use YubiKey + + let (securified_accounts, stats) = os + .securify_accounts( + IndexSet::from_iter([alice.entity_address(), bob.entity_address()]), + MatrixOfFactorSources::new([], 0, [yubikey.clone()]), + ) + .await + .unwrap(); + assert!( + !stats.derived_any_new_instance_for_any_factor_source(), + "should have used cache" + ); + let alice_sec = securified_accounts + .clone() + .into_iter() + .find(|x| x.address() == alice.entity_address()) + .unwrap(); + + let alice_matrix = alice_sec.securified_entity_control().matrix.clone(); + + assert_eq!( + alice_matrix + .all_factors() + .into_iter() + .map(|f| f.derivation_entity_index()) + .collect_vec(), + [ + HDPathComponent::securifying_base_index(1) // Carol used `0`. + ] + ); +} + +#[actix_rt::test] +async fn cache_is_unchanged_in_case_of_failure() { + let (mut os, bdfs) = SargonOS::with_bdfs().await; + + let factor_sources = os.profile_snapshot().factor_sources.clone(); + assert_eq!( + factor_sources.clone().into_iter().collect_vec(), + vec![bdfs.clone(),] + ); + + let n = CACHE_FILLING_QUANTITY / 2; + + for i in 0..3 * n { + let _ = os + .new_mainnet_account_with_bdfs(format!("Acco: {}", i)) + .await + .unwrap(); + } + + let shield_0 = MatrixOfFactorSources::new([bdfs.clone()], 1, []); + + let all_accounts = os + .profile_snapshot() + .get_accounts() + .into_iter() + .collect_vec(); + + let first_half_of_accounts = all_accounts.clone()[0..n] + .iter() + .cloned() + .collect::>(); + + let second_half_of_accounts = all_accounts.clone()[n..3 * n] + .iter() + .cloned() + .collect::>(); + + assert_eq!( + first_half_of_accounts.len() + second_half_of_accounts.len(), + 3 * n + ); + + let (first_half_securified_accounts, stats) = os + .securify_accounts( + first_half_of_accounts + .clone() + .into_iter() + .map(|a| a.entity_address()) + .collect(), + shield_0.clone(), + ) + .await + .unwrap(); + + assert!( + !stats.derived_any_new_instance_for_any_factor_source(), + "should have used cache" + ); + + assert_eq!( + first_half_securified_accounts + .into_iter() + .map(|a| a + .securified_entity_control() + .primary_role_instances() + .into_iter() + .map(|f| f.derivation_entity_index()) + .next() + .unwrap()) // single factor per role text + .collect_vec(), + (0..CACHE_FILLING_QUANTITY / 2) + .map(|i| HDPathComponent::securifying_base_index(i as u32)) + .collect_vec() + ); + + let cache_before_fail = os.cache_snapshot(); + let fail_interactor = Arc::new(TestDerivationInteractors::fail()); // <--- FAIL + + let res = os + .securify_accounts_with_interactor( + fail_interactor, + second_half_of_accounts + .clone() + .into_iter() + .map(|a| a.entity_address()) + .collect(), + shield_0, + ) + .await; + + assert!(res.is_err()); + assert_eq!( + os.cache_snapshot(), + cache_before_fail, + "Cache should not have changed when failing." + ); +} + +#[actix_rt::test] +async fn securify_accounts_when_cache_is_half_full_single_factor_source() { + let (mut os, bdfs) = SargonOS::with_bdfs().await; + + let factor_sources = os.profile_snapshot().factor_sources.clone(); + assert_eq!( + factor_sources.clone().into_iter().collect_vec(), + vec![bdfs.clone(),] + ); + + let n = CACHE_FILLING_QUANTITY / 2; + + for i in 0..3 * n { + let _ = os + .new_mainnet_account_with_bdfs(format!("Acco: {}", i)) + .await + .unwrap(); + } + + let shield_0 = MatrixOfFactorSources::new([bdfs.clone()], 1, []); + + let all_accounts = os + .profile_snapshot() + .get_accounts() + .into_iter() + .collect_vec(); + + let first_half_of_accounts = all_accounts.clone()[0..n] + .iter() + .cloned() + .collect::>(); + + let second_half_of_accounts = all_accounts.clone()[n..3 * n] + .iter() + .cloned() + .collect::>(); + + assert_eq!( + first_half_of_accounts.len() + second_half_of_accounts.len(), + 3 * n + ); + + let (first_half_securified_accounts, stats) = os + .securify_accounts( + first_half_of_accounts + .clone() + .into_iter() + .map(|a| a.entity_address()) + .collect(), + shield_0.clone(), + ) + .await + .unwrap(); + + assert!( + !stats.derived_any_new_instance_for_any_factor_source(), + "should have used cache" + ); + + assert_eq!( + first_half_securified_accounts + .into_iter() + .map(|a| a + .securified_entity_control() + .primary_role_instances() + .into_iter() + .map(|f| f.derivation_entity_index()) + .next() + .unwrap()) // single factor per role text + .collect_vec(), + (0..CACHE_FILLING_QUANTITY / 2) + .map(|i| HDPathComponent::securifying_base_index(i as u32)) + .collect_vec() + ); + + let (second_half_securified_accounts, stats) = os + .securify_accounts( + second_half_of_accounts + .clone() + .into_iter() + .map(|a| a.entity_address()) + .collect(), + shield_0, + ) + .await + .unwrap(); + + assert!( + stats.derived_any_new_instance_for_any_factor_source(), + "should have derived more" + ); + + assert_eq!( + second_half_securified_accounts + .into_iter() + .map(|a| a + .securified_entity_control() + .primary_role_instances() + .into_iter() + .map(|f| f.derivation_entity_index()) + .next() + .unwrap()) // single factor per role text + .collect_vec(), + (CACHE_FILLING_QUANTITY / 2..(CACHE_FILLING_QUANTITY / 2 + CACHE_FILLING_QUANTITY)) + .map(|i| HDPathComponent::securifying_base_index(i as u32)) + .collect_vec() + ); +} + +#[actix_rt::test] +async fn securify_accounts_when_cache_is_half_full_multiple_factor_sources() { + let (mut os, bdfs) = SargonOS::with_bdfs().await; + + let ledger = HDFactorSource::ledger(); + let arculus = HDFactorSource::arculus(); + let yubikey = HDFactorSource::yubikey(); + os.add_factor_source(ledger.clone()).await.unwrap(); + os.add_factor_source(arculus.clone()).await.unwrap(); + os.add_factor_source(yubikey.clone()).await.unwrap(); + + let factor_sources = os.profile_snapshot().factor_sources.clone(); + assert_eq!( + factor_sources.clone().into_iter().collect_vec(), + vec![ + bdfs.clone(), + ledger.clone(), + arculus.clone(), + yubikey.clone(), + ] + ); + + let n = CACHE_FILLING_QUANTITY / 2; + + for i in 0..3 * n { + let (_account, _stats) = os + .new_mainnet_account_with_bdfs(format!("Acco: {}", i)) + .await + .unwrap(); + } + + let shield_0 = + MatrixOfFactorSources::new([bdfs.clone(), ledger.clone(), arculus.clone()], 2, []); + + let all_accounts = os + .profile_snapshot() + .get_accounts() + .into_iter() + .collect_vec(); + + let first_half_of_accounts = all_accounts.clone()[0..n] + .iter() + .cloned() + .collect::>(); + + let second_half_of_accounts = all_accounts.clone()[n..3 * n] + .iter() + .cloned() + .collect::>(); + + assert_eq!( + first_half_of_accounts.len() + second_half_of_accounts.len(), + 3 * n + ); + + let (first_half_securified_accounts, stats) = os + .securify_accounts( + first_half_of_accounts + .clone() + .into_iter() + .map(|a| a.entity_address()) + .collect(), + shield_0.clone(), + ) + .await + .unwrap(); + assert!( + !stats.derived_any_new_instance_for_any_factor_source(), + "should have used cache" + ); + + assert_eq!( + first_half_securified_accounts + .into_iter() + .map(|a| a + .securified_entity_control() + .primary_role_instances() + .into_iter() + .map(|f| f.derivation_entity_index()) + .map(|x| format!("{:?}", x)) + .collect_vec()) + .collect_vec(), + [ + ["0^", "0^", "0^"], + ["1^", "1^", "1^"], + ["2^", "2^", "2^"], + ["3^", "3^", "3^"], + ["4^", "4^", "4^"], + ["5^", "5^", "5^"], + ["6^", "6^", "6^"], + ["7^", "7^", "7^"], + ["8^", "8^", "8^"], + ["9^", "9^", "9^"], + ["10^", "10^", "10^"], + ["11^", "11^", "11^"], + ["12^", "12^", "12^"], + ["13^", "13^", "13^"], + ["14^", "14^", "14^"] + ] + ); + + let (second_half_securified_accounts, stats) = os + .securify_accounts( + second_half_of_accounts + .clone() + .into_iter() + .map(|a| a.entity_address()) + .collect(), + shield_0, + ) + .await + .unwrap(); + + assert!( + stats.derived_any_new_instance_for_any_factor_source(), + "should have derived more" + ); + + assert!( + stats.found_any_instances_in_cache_for_any_factor_source(), + "should have found some in cache" + ); + + assert_eq!( + second_half_securified_accounts + .into_iter() + .map(|a| a + .securified_entity_control() + .primary_role_instances() + .into_iter() + .map(|f| f.derivation_entity_index()) + .map(|x| format!("{:?}", x)) + .collect_vec()) + .collect_vec(), + [ + ["15^", "15^", "15^"], + ["16^", "16^", "16^"], + ["17^", "17^", "17^"], + ["18^", "18^", "18^"], + ["19^", "19^", "19^"], + ["20^", "20^", "20^"], + ["21^", "21^", "21^"], + ["22^", "22^", "22^"], + ["23^", "23^", "23^"], + ["24^", "24^", "24^"], + ["25^", "25^", "25^"], + ["26^", "26^", "26^"], + ["27^", "27^", "27^"], + ["28^", "28^", "28^"], + ["29^", "29^", "29^"], + ["30^", "30^", "30^"], + ["31^", "31^", "31^"], + ["32^", "32^", "32^"], + ["33^", "33^", "33^"], + ["34^", "34^", "34^"], + ["35^", "35^", "35^"], + ["36^", "36^", "36^"], + ["37^", "37^", "37^"], + ["38^", "38^", "38^"], + ["39^", "39^", "39^"], + ["40^", "40^", "40^"], + ["41^", "41^", "41^"], + ["42^", "42^", "42^"], + ["43^", "43^", "43^"], + ["44^", "44^", "44^"] + ] + ); +} + +#[actix_rt::test] +async fn securify_personas_when_cache_is_half_full_single_factor_source() { + let (mut os, bdfs) = SargonOS::with_bdfs().await; + + let factor_sources = os.profile_snapshot().factor_sources.clone(); + assert_eq!( + factor_sources.clone().into_iter().collect_vec(), + vec![bdfs.clone(),] + ); + + let n = CACHE_FILLING_QUANTITY / 2; + + for i in 0..3 * n { + let _ = os + .new_mainnet_persona_with_bdfs(format!("Persona: {}", i)) + .await + .unwrap(); + } + + let shield_0 = MatrixOfFactorSources::new([bdfs.clone()], 1, []); + + let all_personas = os + .profile_snapshot() + .get_personas() + .into_iter() + .collect_vec(); + + let first_half_of_personas = all_personas.clone()[0..n] + .iter() + .cloned() + .collect::>(); + + let second_half_of_personas = all_personas.clone()[n..3 * n] + .iter() + .cloned() + .collect::>(); + + assert_eq!( + first_half_of_personas.len() + second_half_of_personas.len(), + 3 * n + ); + + let (first_half_securified_personas, stats) = os + .securify_personas( + first_half_of_personas + .clone() + .into_iter() + .map(|a| a.entity_address()) + .collect(), + shield_0.clone(), + ) + .await + .unwrap(); + + assert!( + !stats.derived_any_new_instance_for_any_factor_source(), + "should have used cache" + ); + + assert_eq!( + first_half_securified_personas + .into_iter() + .map(|a| a + .securified_entity_control() + .primary_role_instances() + .into_iter() + .map(|f| f.derivation_entity_index()) + .map(|x| format!("{:?}", x)) + .next() + .unwrap()) // single factor per role text + .collect_vec(), + [ + "0^", "1^", "2^", "3^", "4^", "5^", "6^", "7^", "8^", "9^", "10^", "11^", "12^", "13^", + "14^" + ] + ); + + let (second_half_securified_personas, stats) = os + .securify_personas( + second_half_of_personas + .clone() + .into_iter() + .map(|a| a.entity_address()) + .collect(), + shield_0, + ) + .await + .unwrap(); + + assert!( + stats.derived_any_new_instance_for_any_factor_source(), + "should have derived more" + ); + + assert_eq!( + second_half_securified_personas + .into_iter() + .map(|a| a + .securified_entity_control() + .primary_role_instances() + .into_iter() + .map(|f| f.derivation_entity_index()) + .map(|x| format!("{:?}", x)) + .next() + .unwrap()) // single factor per role text + .collect_vec(), + [ + "15^", "16^", "17^", "18^", "19^", "20^", "21^", "22^", "23^", "24^", "25^", "26^", + "27^", "28^", "29^", "30^", "31^", "32^", "33^", "34^", "35^", "36^", "37^", "38^", + "39^", "40^", "41^", "42^", "43^", "44^" + ] + ); +} + +#[actix_rt::test] +async fn create_single_account() { + let (mut os, bdfs) = SargonOS::with_bdfs().await; + let (alice, stats) = os.new_mainnet_account_with_bdfs("alice").await.unwrap(); + assert!(stats.debug_was_derived.is_empty(), "should have used cache"); + let (sec_accounts, stats) = os + .securify_accounts( + IndexSet::just(alice.entity_address()), + MatrixOfFactorSources::new([], 0, [bdfs]), + ) + .await + .unwrap(); + assert!( + !stats.derived_any_new_instance_for_any_factor_source(), + "should have used cache" + ); + let alice_sec = sec_accounts.into_iter().next().unwrap(); + assert_eq!( + alice_sec + .securified_entity_control() + .primary_role_instances() + .first() + .unwrap() + .derivation_entity_index(), + HDPathComponent::securifying_base_index(0) + ); +} + +#[actix_rt::test] +async fn securified_personas() { + let (mut os, bdfs) = SargonOS::with_bdfs().await; + let batman = os + .new_persona_with_bdfs(NetworkID::Mainnet, "Batman") + .await + .unwrap() + .0; + + let satoshi = os + .new_persona_with_bdfs(NetworkID::Mainnet, "Satoshi") + .await + .unwrap() + .0; + assert_ne!(batman.address(), satoshi.address()); + let ledger = HDFactorSource::ledger(); + let arculus = HDFactorSource::arculus(); + let yubikey = HDFactorSource::yubikey(); + os.add_factor_source(ledger.clone()).await.unwrap(); + os.add_factor_source(arculus.clone()).await.unwrap(); + os.add_factor_source(yubikey.clone()).await.unwrap(); + let shield_0 = + MatrixOfFactorSources::new([bdfs.clone(), ledger.clone(), arculus.clone()], 2, []); + + let (securified_personas, stats) = os + .securify_personas( + IndexSet::from_iter([batman.entity_address(), satoshi.entity_address()]), + shield_0, + ) + .await + .unwrap(); + + assert!( + !stats.derived_any_new_instance_for_any_factor_source(), + "should have used cache" + ); + + let batman_sec = securified_personas + .clone() + .into_iter() + .find(|x| x.address() == batman.entity_address()) + .unwrap(); + + assert_eq!( + batman_sec.securified_entity_control().veci.unwrap().clone(), + batman.as_unsecurified().unwrap().veci() + ); + let batman_matrix = batman_sec.securified_entity_control().primary_role(); + assert_eq!(batman_matrix.threshold, 2); + + assert_eq!( + batman_matrix + .all_factors() + .into_iter() + .map(|f| f.factor_source_id()) + .collect_vec(), + [ + bdfs.factor_source_id(), + ledger.factor_source_id(), + arculus.factor_source_id() + ] + ); + + assert_eq!( + batman_matrix + .all_factors() + .into_iter() + .map(|f| f.derivation_entity_index()) + .collect_vec(), + [ + HDPathComponent::securifying_base_index(0), + HDPathComponent::securifying_base_index(0), + HDPathComponent::securifying_base_index(0) + ] + ); + + // assert satoshi + + let satoshi_sec = securified_personas + .clone() + .into_iter() + .find(|x| x.address() == satoshi.entity_address()) + .unwrap(); + + assert_eq!( + satoshi_sec + .securified_entity_control() + .veci + .unwrap() + .clone(), + satoshi.as_unsecurified().unwrap().veci() + ); + let satoshi_matrix = satoshi_sec.securified_entity_control().primary_role(); + assert_eq!(satoshi_matrix.threshold, 2); + + assert_eq!( + satoshi_matrix + .all_factors() + .into_iter() + .map(|f| f.factor_source_id()) + .collect_vec(), + [ + bdfs.factor_source_id(), + ledger.factor_source_id(), + arculus.factor_source_id() + ] + ); + + assert_eq!( + satoshi_matrix + .all_factors() + .into_iter() + .map(|f| f.derivation_entity_index()) + .collect_vec(), + [ + HDPathComponent::securifying_base_index(1), + HDPathComponent::securifying_base_index(1), + HDPathComponent::securifying_base_index(1) + ] + ); + + let hyde = os + .new_persona(ledger.clone(), NetworkID::Mainnet, "Mr Hyde") + .await + .unwrap() + .0; + + assert_eq!( + hyde + .as_unsecurified() + .unwrap() + .veci() + .factor_instance() + .derivation_entity_index() + .base_index(), + 0, + "First persona created with ledger, should have index 0, even though this ledger was used in the shield, since we are using two different KeySpaces for Securified and Unsecurified personas." + ); + + let (securified_personas, stats) = os + .securify_personas( + IndexSet::just(hyde.entity_address()), + MatrixOfFactorSources::new([], 0, [yubikey.clone()]), + ) + .await + .unwrap(); + assert!( + !stats.derived_any_new_instance_for_any_factor_source(), + "should have used cache" + ); + let hyde_sec = securified_personas + .clone() + .into_iter() + .find(|x| x.address() == hyde.entity_address()) + .unwrap(); + + let hyde_matrix = hyde_sec.securified_entity_control().primary_role(); + + assert_eq!( + hyde_matrix + .all_factors() + .into_iter() + .map(|f| f.factor_source_id()) + .collect_vec(), + [yubikey.factor_source_id()] + ); + + assert_eq!( + hyde_matrix + .all_factors() + .into_iter() + .map(|f| f.derivation_entity_index()) + .collect_vec(), + [HDPathComponent::securifying_base_index(0)] + ); + + // Update Batmans and Satoshis's shield to only use YubiKey + + let (securified_personas, stats) = os + .securify_personas( + IndexSet::from_iter([batman.entity_address(), satoshi.entity_address()]), + MatrixOfFactorSources::new([], 0, [yubikey.clone()]), + ) + .await + .unwrap(); + assert!( + !stats.derived_any_new_instance_for_any_factor_source(), + "should have used cache" + ); + let batman_sec = securified_personas + .clone() + .into_iter() + .find(|x| x.address() == batman.entity_address()) + .unwrap(); + + let batman_matrix = batman_sec.securified_entity_control().primary_role(); + + assert_eq!( + batman_matrix + .all_factors() + .into_iter() + .map(|f| f.derivation_entity_index()) + .collect_vec(), + [HDPathComponent::securifying_base_index(1)] + ); +} + +#[actix_rt::test] +async fn securified_all_accounts_next_veci_does_not_start_at_zero() { + let mut os = SargonOS::new(); + assert_eq!(os.cache_snapshot().total_number_of_factor_instances(), 0); + + let fs_device = HDFactorSource::device(); + let fs_arculus = HDFactorSource::arculus(); + let fs_ledger = HDFactorSource::ledger(); + + os.add_factor_source(fs_device.clone()).await.unwrap(); + os.add_factor_source(fs_arculus.clone()).await.unwrap(); + os.add_factor_source(fs_ledger.clone()).await.unwrap(); + + assert_eq!( + os.cache_snapshot().total_number_of_factor_instances(), + 3 * 4 * CACHE_FILLING_QUANTITY + ); + + assert!(os.profile_snapshot().get_accounts().is_empty()); + assert_eq!(os.profile_snapshot().factor_sources.len(), 3); + + let network = NetworkID::Mainnet; + + // first create CACHE_FILLING_QUANTITY many "unnamed" accounts + + for i in 0..CACHE_FILLING_QUANTITY { + let (_, stats) = os + .new_account(fs_device.clone(), network, format!("@{}", i)) + .await + .unwrap(); + assert!(stats.debug_was_derived.is_empty()); + } + + let unnamed_accounts = os + .profile_snapshot() + .get_accounts() + .into_iter() + .collect_vec(); + + let (_, stats) = os + .securify_accounts( + unnamed_accounts + .clone() + .into_iter() + .map(|a| a.entity_address()) + .collect(), + MatrixOfFactorSources::new([fs_device.clone()], 1, []), + ) + .await + .unwrap(); + + assert!( + !stats.derived_any_new_instance_for_any_factor_source(), + "should have used cache" + ); + + // assert correctness of next index assigner + assert_eq!( + os.profile_snapshot().accounts_on_network(network).len(), + CACHE_FILLING_QUANTITY + ); + + let next_index_profile_assigner = + NextDerivationEntityIndexProfileAnalyzingAssigner::new(network, os.profile_snapshot()); + let next_index = next_index_profile_assigner + .next( + fs_device.factor_source_id(), + DerivationPreset::AccountVeci.index_agnostic_path_on_network(network), + ) + .unwrap() + .unwrap(); + assert_eq!( + next_index, + HDPathComponent::unsecurified_hardening_base_index(30) + ); + + let (alice, stats) = os + .new_account(fs_device.clone(), network, "Alice") + .await + .unwrap(); + assert!( + stats.debug_found_in_cache.is_empty(), + "Cache should have been empty" + ); + assert!( + !stats.debug_was_derived.is_empty(), + "should have filled cache" + ); + + assert_eq!( + alice + .as_unsecurified() + .unwrap() + .veci() + .factor_instance() + .derivation_entity_index(), + HDPathComponent::unsecurified_hardening_base_index(30) // <-- IMPORTANT this tests that we do not start at 0', asserts that the next index from profile analyzer + ); + + // later when securified we want the next index in securified key space to be 30^ + let (securified_alice, stats) = os + .securify_account( + alice.entity_address(), + MatrixOfFactorSources::new([], 0, [fs_device.clone()]), + ) + .await + .unwrap(); + assert!(stats.found_any_instances_in_cache_for_any_factor_source()); + assert!(!stats.derived_any_new_instance_for_any_factor_source()); + + assert_eq!( + securified_alice + .securified_entity_control() + .primary_role_instances() + .into_iter() + .map(|f| (f.factor_source_id(), f.derivation_entity_index())) + .collect::>(), + [( + fs_device.factor_source_id(), + HDPathComponent::securifying_base_index(30) + )] + .into_iter() + .collect::>() + ); +} + +#[actix_rt::test] +async fn securified_accounts_asymmetric_indices() { + let mut os = SargonOS::new(); + assert_eq!(os.cache_snapshot().total_number_of_factor_instances(), 0); + + let fs_device = HDFactorSource::device(); + let fs_arculus = HDFactorSource::arculus(); + let fs_ledger = HDFactorSource::ledger(); + + os.add_factor_source(fs_device.clone()).await.unwrap(); + os.add_factor_source(fs_arculus.clone()).await.unwrap(); + os.add_factor_source(fs_ledger.clone()).await.unwrap(); + + assert_eq!( + os.cache_snapshot().total_number_of_factor_instances(), + 3 * 4 * CACHE_FILLING_QUANTITY + ); + + assert!(os.profile_snapshot().get_accounts().is_empty()); + assert_eq!(os.profile_snapshot().factor_sources.len(), 3); + + let network = NetworkID::Mainnet; + + // first create CACHE_FILLING_QUANTITY many "unnamed" accounts + + for i in 0..CACHE_FILLING_QUANTITY { + let (_, stats) = os + .new_account(fs_device.clone(), network, format!("@{}", i)) + .await + .unwrap(); + assert!(stats.debug_was_derived.is_empty()); + } + + let unnamed_accounts = os + .profile_snapshot() + .get_accounts() + .into_iter() + .collect_vec(); + + let (_, stats) = os + .securify_accounts( + unnamed_accounts + .clone() + .into_iter() + .map(|a| a.entity_address()) + .collect(), + MatrixOfFactorSources::new([fs_device.clone()], 1, []), + ) + .await + .unwrap(); + + assert!( + !stats.derived_any_new_instance_for_any_factor_source(), + "should have used cache" + ); + + let (alice, stats) = os + .new_account(fs_device.clone(), network, "Alice") + .await + .unwrap(); + assert!( + stats.debug_found_in_cache.is_empty(), + "Cache should have been empty" + ); + assert!( + !stats.debug_was_derived.is_empty(), + "should have filled cache" + ); + + let (bob, _) = os + .new_account(fs_device.clone(), network, "Bob") + .await + .unwrap(); + + let (carol, _) = os + .new_account(fs_device.clone(), network, "Carol") + .await + .unwrap(); + + let (diana, _) = os + .new_account(fs_device.clone(), network, "Diana") + .await + .unwrap(); + + assert_eq!( + diana + .as_unsecurified() + .unwrap() + .veci() + .factor_instance() + .derivation_entity_index(), + HDPathComponent::unsecurified_hardening_base_index(33) + ); + + let (securified_alice, stats) = os + .securify_account( + alice.entity_address(), + MatrixOfFactorSources::new([], 0, [fs_device.clone(), fs_arculus.clone()]), + ) + .await + .unwrap(); + assert!(stats.found_any_instances_in_cache_for_any_factor_source()); + assert!(!stats.derived_any_new_instance_for_any_factor_source()); + + assert_eq!( + securified_alice + .securified_entity_control() + .primary_role_instances() + .into_iter() + .map(|f| (f.factor_source_id(), f.derivation_entity_index())) + .collect::>(), + [ + ( + fs_device.factor_source_id(), + HDPathComponent::securifying_base_index(30) + ), + ( + fs_arculus.factor_source_id(), + HDPathComponent::securifying_base_index(0) + ), + ] + .into_iter() + .collect::>() + ); + + let (securified_bob, stats) = os + .securify_account( + bob.entity_address(), + MatrixOfFactorSources::new([], 0, [fs_device.clone(), fs_ledger.clone()]), + ) + .await + .unwrap(); + assert!(stats.found_any_instances_in_cache_for_any_factor_source()); + assert!(!stats.derived_any_new_instance_for_any_factor_source()); + + assert_eq!( + securified_bob + .securified_entity_control() + .primary_role_instances() + .into_iter() + .map(|f| (f.factor_source_id(), f.derivation_entity_index())) + .collect::>(), + [ + ( + fs_device.factor_source_id(), + HDPathComponent::securifying_base_index(31) + ), + ( + fs_ledger.factor_source_id(), + HDPathComponent::securifying_base_index(0) + ), + ] + .into_iter() + .collect::>() + ); + + let (securified_carol, stats) = os + .securify_account( + carol.entity_address(), + MatrixOfFactorSources::new([], 0, [fs_device.clone(), fs_arculus.clone()]), + ) + .await + .unwrap(); + assert!(stats.found_any_instances_in_cache_for_any_factor_source()); + assert!(!stats.derived_any_new_instance_for_any_factor_source()); + + assert_eq!( + securified_carol + .securified_entity_control() + .primary_role_instances() + .into_iter() + .map(|f| (f.factor_source_id(), f.derivation_entity_index())) + .collect::>(), + [ + ( + fs_device.factor_source_id(), + HDPathComponent::securifying_base_index(32) + ), + ( + fs_arculus.factor_source_id(), + HDPathComponent::securifying_base_index(1) + ), + ] + .into_iter() + .collect::>() + ); + + // CLEAR CACHE + os.clear_cache(); + + let shield_3fa = MatrixOfFactorSources::new( + [], + 0, + [fs_device.clone(), fs_arculus.clone(), fs_ledger.clone()], + ); + let (securified_diana, stats) = os + .securify_account(diana.entity_address(), shield_3fa.clone()) + .await + .unwrap(); + assert!(!stats.found_any_instances_in_cache_for_any_factor_source()); + assert!(stats.derived_any_new_instance_for_any_factor_source()); + + let diana_mfa_device = 33; + let diana_mfa_arculus = 2; + let diana_mfa_ledger = 1; + + assert_eq!( + securified_diana + .securified_entity_control() + .primary_role_instances() + .into_iter() + .map(|f| (f.factor_source_id(), f.derivation_entity_index())) + .collect::>(), + [ + ( + fs_device.factor_source_id(), + HDPathComponent::securifying_base_index(diana_mfa_device) + ), + ( + fs_arculus.factor_source_id(), + HDPathComponent::securifying_base_index(diana_mfa_arculus) + ), + ( + fs_ledger.factor_source_id(), + HDPathComponent::securifying_base_index(diana_mfa_ledger) + ), + ] + .into_iter() + .collect::>() + ); + + // lets create 2 * CACHE_FILLING_QUANTITY many more accounts and securify them with + // the same shield as Diana + + os.clear_cache(); // CLEAR CACHE + let mut more_unnamed_accounts = IndexSet::new(); + for i in 0..2 * CACHE_FILLING_QUANTITY { + let (unnamed, _) = os + .new_account(fs_device.clone(), network, format!("more@{}", i)) + .await + .unwrap(); + more_unnamed_accounts.insert(unnamed.entity_address()); + } + + let (many_securified_accounts, stats) = os + .securify_accounts(more_unnamed_accounts.clone(), shield_3fa.clone()) + .await + .unwrap(); + assert!( + stats.derived_any_new_instance_for_any_factor_source(), + "twice the cache size => derive more" + ); + os.clear_cache(); // CLEAR CACHE + for index in 0..many_securified_accounts.len() { + let securified_account = many_securified_accounts + .clone() + .into_iter() + .nth(index) + .unwrap(); + let offset = (index + 1) as HDPathValue; + assert_eq!( + securified_account + .securified_entity_control() + .primary_role_instances() + .into_iter() + .map(|f| (f.factor_source_id(), f.derivation_entity_index())) + .collect::>(), + [ + ( + fs_device.factor_source_id(), + HDPathComponent::securifying_base_index(diana_mfa_device + offset) + ), + ( + fs_arculus.factor_source_id(), + HDPathComponent::securifying_base_index(diana_mfa_arculus + offset) + ), + ( + fs_ledger.factor_source_id(), + HDPathComponent::securifying_base_index(diana_mfa_ledger + offset) + ), + ] + .into_iter() + .collect::>() + ); + } +} diff --git a/src/factor_instances_provider/provider/keyed_instances.rs b/src/factor_instances_provider/provider/keyed_instances.rs new file mode 100644 index 00000000..4bed7211 --- /dev/null +++ b/src/factor_instances_provider/provider/keyed_instances.rs @@ -0,0 +1,76 @@ +use std::borrow::Borrow; + +use crate::prelude::*; + +pub struct KeyedInstances(pub IndexMap); + +impl KeyedInstances { + pub fn validate_from_source( + &self, + factor_source_id: impl Borrow, + ) -> Result<()> { + if self + .all_instances() + .into_iter() + .any(|f| f.factor_source_id != *factor_source_id.borrow()) + { + return Err(CommonError::FactorSourceDiscrepancy); + } + Ok(()) + } + + pub fn remove(&mut self, key: impl Borrow) -> Option { + self.0.shift_remove(key.borrow()) + } + pub fn all_instances(&self) -> FactorInstances { + self.0 + .clone() + .into_iter() + .flat_map(|(_, v)| v.factor_instances()) + .collect::() + } + pub fn new(map: IndexMap) -> Self { + Self(map) + } +} + +impl IntoIterator for KeyedInstances { + type Item = as IntoIterator>::Item; + type IntoIter = as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +pub type InstancesByAgnosticPath = KeyedInstances; +pub type InstancesByDerivationPreset = KeyedInstances; +impl InstancesByAgnosticPath { + pub fn into_derivation_preset(self) -> InstancesByDerivationPreset { + let map = self + .into_iter() + .map(|(k, v)| (DerivationPreset::try_from(k).unwrap(), v)) + .collect::>(); + InstancesByDerivationPreset::new(map) + } +} + +impl From for InstancesByAgnosticPath { + fn from(value: FactorInstances) -> Self { + let map = value + .factor_instances() + .into_iter() + .into_group_map_by(|f| f.agnostic_path()) + .into_iter() + .map(|(k, v)| (k, FactorInstances::from_iter(v))) + .collect::>(); + + Self::new(map) + } +} + +impl From for InstancesByDerivationPreset { + fn from(value: FactorInstances) -> Self { + InstancesByAgnosticPath::from(value).into_derivation_preset() + } +} diff --git a/src/factor_instances_provider/provider/mod.rs b/src/factor_instances_provider/provider/mod.rs new file mode 100644 index 00000000..80646312 --- /dev/null +++ b/src/factor_instances_provider/provider/mod.rs @@ -0,0 +1,16 @@ +mod factor_instances_cache; +mod factor_instances_provider; +mod keyed_instances; +mod outcome; +mod provider_adopters; + +#[cfg(test)] +mod factor_instances_provider_unit_tests; +#[cfg(test)] +mod test_sargon_os; + +pub use factor_instances_cache::*; +pub use factor_instances_provider::*; +pub use keyed_instances::*; +pub use outcome::*; +pub use provider_adopters::*; diff --git a/src/factor_instances_provider/provider/outcome/factor_instances_provider_outcome.rs b/src/factor_instances_provider/provider/outcome/factor_instances_provider_outcome.rs new file mode 100644 index 00000000..8aaa532e --- /dev/null +++ b/src/factor_instances_provider/provider/outcome/factor_instances_provider_outcome.rs @@ -0,0 +1,54 @@ +use crate::prelude::*; + +/// Identical to `InternalFactorInstancesProviderOutcome` but `FactorInstancesProviderOutcomeForFactor` instead of `InternalFactorInstancesProviderOutcomeForFactor`, having +/// renamed field values to make it clear that `to_cache` instances already have been cached. +#[derive(Clone, Debug)] +pub struct FactorInstancesProviderOutcome { + pub per_factor: IndexMap, +} + +impl From for FactorInstancesProviderOutcome { + fn from(value: InternalFactorInstancesProviderOutcome) -> Self { + Self { + per_factor: value + .per_factor + .into_iter() + .map(|(k, v)| (k, v.into())) + .collect(), + } + } +} + +#[cfg(test)] +impl FactorInstancesProviderOutcome { + pub fn newly_derived_instances_from_all_factor_sources(&self) -> FactorInstances { + self.per_factor + .values() + .flat_map(|x| x.debug_was_derived.factor_instances()) + .collect() + } + + pub fn total_number_of_newly_derived_instances(&self) -> usize { + self.newly_derived_instances_from_all_factor_sources().len() + } + + pub fn derived_any_new_instance_for_any_factor_source(&self) -> bool { + self.total_number_of_newly_derived_instances() > 0 + } + + pub fn instances_found_in_cache_from_all_factor_sources(&self) -> FactorInstances { + self.per_factor + .values() + .flat_map(|x| x.debug_found_in_cache.factor_instances()) + .collect() + } + + pub fn total_number_of_instances_found_in_cache(&self) -> usize { + self.instances_found_in_cache_from_all_factor_sources() + .len() + } + + pub fn found_any_instances_in_cache_for_any_factor_source(&self) -> bool { + self.total_number_of_instances_found_in_cache() > 0 + } +} diff --git a/src/factor_instances_provider/provider/outcome/factor_instances_provider_outcome_for_factor.rs b/src/factor_instances_provider/provider/outcome/factor_instances_provider_outcome_for_factor.rs new file mode 100644 index 00000000..44e628a0 --- /dev/null +++ b/src/factor_instances_provider/provider/outcome/factor_instances_provider_outcome_for_factor.rs @@ -0,0 +1,101 @@ +use crate::prelude::*; + +/// Identical to `InternalFactorInstancesProviderOutcomeForFactor` but with +/// different field names, making it clear that the instances of `to_cache` field in the +/// "non-final" counterpart has already been cached, thus here named +/// `debug_was_cached`. +/// Furthermore all fields except `to_use_directly` are renamed to `debug_*` to make it clear they are only included for debugging purposes, +/// in fact, they are all put behind `#[cfg(test)]` +#[derive(Clone, derive_more::Debug)] +#[debug("{}", self.debug_string())] +pub struct FactorInstancesProviderOutcomeForFactor { + #[allow(dead_code)] + hidden: HiddenConstructor, + + /// The FactorSourceID of all the factor instances of this type. + pub factor_source_id: FactorSourceIDFromHash, + + /// FactorInstances which are not saved into the cache. + /// + /// Might be empty + pub to_use_directly: FactorInstances, + + /// FactorInstances which were saved into the cache + /// + /// Might be empty + /// + /// Useful for unit tests. + #[cfg(test)] + pub debug_was_cached: FactorInstances, + + /// FactorInstances which was found in the cache before the operation was + /// executed. + /// + /// Might be empty + /// + /// Useful for unit tests. + /// + /// Might overlap with `to_use_directly` + #[cfg(test)] + pub debug_found_in_cache: FactorInstances, + + /// FactorInstances which was derived. + /// + /// Might be empty + /// + /// Useful for unit tests. + /// + /// Might overlap with `to_cache` and `to_use_directly` + #[cfg(test)] + pub debug_was_derived: FactorInstances, +} +#[allow(dead_code)] +impl FactorInstancesProviderOutcomeForFactor { + #[cfg(test)] + fn debug_string_for_tests(&self) -> String { + format!( + "OutcomeForFactor[factor: {}\n\n\t⚡️to_use_directly: {:?}, \n\n\t➡️💾was_cached: {:?}, \n\n\t💾➡️found_in_cache: {:?}\n\n\t🔮was_derived: {:?}\n\n]", + self.factor_source_id, self.to_use_directly, self.debug_was_cached, self.debug_found_in_cache, self.debug_was_derived + ) + } + + fn debug_string_no_test(&self) -> String { + format!( + "OutcomeForFactor[factor: {}, \n\n\t⚡️to_use_directly: {:?}]", + self.factor_source_id, self.to_use_directly + ) + } + + fn debug_string(&self) -> String { + #[cfg(test)] + return self.debug_string_for_tests(); + + #[cfg(not(test))] + return self.debug_string_no_test(); + } +} + +impl From + for FactorInstancesProviderOutcomeForFactor +{ + fn from(value: InternalFactorInstancesProviderOutcomeForFactor) -> Self { + #[cfg(test)] + let _self = Self { + hidden: HiddenConstructor, + factor_source_id: value.factor_source_id, + to_use_directly: value.to_use_directly, + debug_was_cached: value.to_cache, + debug_found_in_cache: value.found_in_cache, + debug_was_derived: value.newly_derived, + }; + + #[cfg(not(test))] + let _self = Self { + hidden: HiddenConstructor, + factor_source_id: value.factor_source_id, + to_use_directly: value.to_use_directly, + }; + + _self + } +} diff --git a/src/factor_instances_provider/provider/outcome/internal_factor_instances_provider_outcome.rs b/src/factor_instances_provider/provider/outcome/internal_factor_instances_provider_outcome.rs new file mode 100644 index 00000000..48b848e3 --- /dev/null +++ b/src/factor_instances_provider/provider/outcome/internal_factor_instances_provider_outcome.rs @@ -0,0 +1,221 @@ +use crate::prelude::*; + +#[derive(Clone, Debug)] +pub struct InternalFactorInstancesProviderOutcome { + pub per_factor: + IndexMap, +} + +impl InternalFactorInstancesProviderOutcome { + pub fn new( + per_factor: IndexMap< + FactorSourceIDFromHash, + InternalFactorInstancesProviderOutcomeForFactor, + >, + ) -> Self { + Self { per_factor } + } + + /// Outcome of FactorInstances just from cache, none have been derived. + pub fn satisfied_by_cache( + pf_found_in_cache: IndexMap, + ) -> Self { + Self::new( + pf_found_in_cache + .into_iter() + .map(|(k, v)| { + ( + k, + InternalFactorInstancesProviderOutcomeForFactor::satisfied_by_cache(k, v), + ) + }) + .collect(), + ) + } + + /// "Transposes" a **collection** of `IndexMap` into `IndexMap` (`InternalFactorInstancesProviderOutcomeForFactor` is essentially a collection of FactorInstance) + pub fn transpose( + pf_to_cache: IndexMap, + pf_to_use_directly: IndexMap, + pf_found_in_cache: IndexMap, + pf_newly_derived: IndexMap, + ) -> Self { + struct Builder { + factor_source_id: FactorSourceIDFromHash, + + /// Might be empty + pub to_cache: IndexSet, + /// Might be empty + pub to_use_directly: IndexSet, + + /// LESS IMPORTANT - for tests... + /// might overlap with `to_use_directly` + pub found_in_cache: IndexSet, + /// might overlap with `to_cache` and `to_use_directly` + pub newly_derived: IndexSet, + } + impl Builder { + fn build(self) -> InternalFactorInstancesProviderOutcomeForFactor { + let to_cache = FactorInstances::from(self.to_cache); + let to_use_directly = FactorInstances::from(self.to_use_directly); + let found_in_cache = FactorInstances::from(self.found_in_cache); + let newly_derived = FactorInstances::from(self.newly_derived); + InternalFactorInstancesProviderOutcomeForFactor::new( + self.factor_source_id, + to_cache, + to_use_directly, + found_in_cache, + newly_derived, + ) + } + fn new(factor_source_id: FactorSourceIDFromHash) -> Self { + Self { + factor_source_id, + to_cache: IndexSet::new(), + to_use_directly: IndexSet::new(), + found_in_cache: IndexSet::new(), + newly_derived: IndexSet::new(), + } + } + } + let mut builders = IndexMap::::new(); + + for (factor_source_id, instances) in pf_found_in_cache { + if let Some(builder) = builders.get_mut(&factor_source_id) { + builder.found_in_cache.extend(instances.factor_instances()); + } else { + let mut builder = Builder::new(factor_source_id); + builder.found_in_cache.extend(instances.factor_instances()); + builders.insert(factor_source_id, builder); + } + } + + for (factor_source_id, instances) in pf_newly_derived { + if let Some(builder) = builders.get_mut(&factor_source_id) { + builder.newly_derived.extend(instances.factor_instances()); + } else { + let mut builder = Builder::new(factor_source_id); + builder.newly_derived.extend(instances.factor_instances()); + builders.insert(factor_source_id, builder); + } + } + + for (factor_source_id, instances) in pf_to_cache { + if let Some(builder) = builders.get_mut(&factor_source_id) { + builder.to_cache.extend(instances.factor_instances()); + } else { + let mut builder = Builder::new(factor_source_id); + builder.to_cache.extend(instances.factor_instances()); + builders.insert(factor_source_id, builder); + } + } + + for (factor_source_id, instances) in pf_to_use_directly { + if let Some(builder) = builders.get_mut(&factor_source_id) { + builder.to_use_directly.extend(instances.factor_instances()); + } else { + let mut builder = Builder::new(factor_source_id); + builder.to_use_directly.extend(instances.factor_instances()); + builders.insert(factor_source_id, builder); + } + } + + Self::new( + builders + .into_iter() + .map(|(k, v)| (k, v.build())) + .collect::>(), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + type Sut = InternalFactorInstancesProviderOutcome; + + #[test] + fn only_to_cache() { + let i = HierarchicalDeterministicFactorInstance::fia0(); + + let sut = Sut::transpose( + IndexMap::kv( + FactorSourceIDFromHash::fs0(), + FactorInstances::just(i.clone()), + ), + IndexMap::new(), + IndexMap::new(), + IndexMap::new(), + ); + assert_eq!( + sut.per_factor.get(&i.factor_source_id()).unwrap().to_cache, + FactorInstances::just(i) + ) + } + + #[test] + fn only_to_use_directly() { + let i = HierarchicalDeterministicFactorInstance::fia0(); + + let sut = Sut::transpose( + IndexMap::new(), + IndexMap::kv( + FactorSourceIDFromHash::fs0(), + FactorInstances::just(i.clone()), + ), + IndexMap::new(), + IndexMap::new(), + ); + assert_eq!( + sut.per_factor + .get(&i.factor_source_id()) + .unwrap() + .to_use_directly, + FactorInstances::just(i) + ) + } + + #[test] + fn only_found_in_cache() { + let i = HierarchicalDeterministicFactorInstance::fia0(); + + let sut = Sut::transpose( + IndexMap::new(), + IndexMap::new(), + IndexMap::kv( + FactorSourceIDFromHash::fs0(), + FactorInstances::just(i.clone()), + ), + IndexMap::new(), + ); + assert_eq!( + sut.per_factor + .get(&i.factor_source_id()) + .unwrap() + .found_in_cache, + FactorInstances::just(i) + ) + } + + #[test] + fn only_newly_derived() { + let i = HierarchicalDeterministicFactorInstance::fia0(); + + let sut = Sut::transpose( + IndexMap::new(), + IndexMap::new(), + IndexMap::new(), + IndexMap::kv( + FactorSourceIDFromHash::fs0(), + FactorInstances::just(i.clone()), + ), + ); + assert_eq!( + sut.per_factor + .get(&i.factor_source_id()) + .unwrap() + .newly_derived, + FactorInstances::just(i) + ) + } +} diff --git a/src/factor_instances_provider/provider/outcome/internal_factor_instances_provider_outcome_for_factor.rs b/src/factor_instances_provider/provider/outcome/internal_factor_instances_provider_outcome_for_factor.rs new file mode 100644 index 00000000..00ad8e6f --- /dev/null +++ b/src/factor_instances_provider/provider/outcome/internal_factor_instances_provider_outcome_for_factor.rs @@ -0,0 +1,100 @@ +use crate::prelude::*; + +#[derive(Clone, derive_more::Debug)] +#[debug( + "InternalFactorInstancesProviderOutcomeForFactor[ factor: {:?}\n\n\t⚡️ to_use_directly: {:?}\n\n\t➡️💾to_cache: {:?}\n\n\t💾➡️found_in_cache: {:?}\n\n\t🔮derived: {:?}\n\n]\n", + factor_source_id, + to_use_directly, + to_cache, + found_in_cache, + newly_derived +)] +pub struct InternalFactorInstancesProviderOutcomeForFactor { + #[allow(dead_code)] + hidden: HiddenConstructor, + + /// The FactorSourceID of all the factor instances of this type. + pub factor_source_id: FactorSourceIDFromHash, + + /// FactorInstances which are saved into the cache + /// + /// Might be empty + pub to_cache: FactorInstances, + + /// FactorInstances which are not saved into the cache. + /// + /// Might be empty + pub to_use_directly: FactorInstances, + + /// FactorInstances which was found in the cache before the operation was + /// executed. + /// + /// Might be empty + /// + /// Useful for unit tests. + /// + /// Might overlap with `to_use_directly` + pub found_in_cache: FactorInstances, + + /// FactorInstances which was newly derived. + /// + /// Might be empty + /// + /// Useful for unit tests. + /// + /// Might overlap with `to_cache` and `to_use_directly` + pub newly_derived: FactorInstances, +} + +impl InternalFactorInstancesProviderOutcomeForFactor { + pub fn new( + factor_source_id: FactorSourceIDFromHash, + to_cache: FactorInstances, + to_use_directly: FactorInstances, + found_in_cache: FactorInstances, + newly_derived: FactorInstances, + ) -> Self { + let assert_factor = |xs: &FactorInstances| { + assert!( + xs.factor_instances() + .iter() + .all(|x| x.factor_source_id() == factor_source_id), + "Discrepancy factor source id" + ); + }; + assert_factor(&to_cache); + assert_factor(&to_use_directly); + assert_factor(&found_in_cache); + assert_factor(&newly_derived); + + Self { + hidden: HiddenConstructor, + factor_source_id, + to_cache, + to_use_directly, + found_in_cache, + newly_derived, + } + } + + pub fn satisfied_by_cache( + factor_source_id: FactorSourceIDFromHash, + found_in_cache: FactorInstances, + ) -> Self { + let to_use_directly = found_in_cache.clone(); + + // nothing to cache + let to_cache = FactorInstances::default(); + + // nothing was derived + let newly_derived = FactorInstances::default(); + + Self::new( + factor_source_id, + to_cache, + to_use_directly, + found_in_cache, + newly_derived, + ) + } +} diff --git a/src/factor_instances_provider/provider/outcome/mod.rs b/src/factor_instances_provider/provider/outcome/mod.rs new file mode 100644 index 00000000..846c204d --- /dev/null +++ b/src/factor_instances_provider/provider/outcome/mod.rs @@ -0,0 +1,9 @@ +mod factor_instances_provider_outcome; +mod factor_instances_provider_outcome_for_factor; +mod internal_factor_instances_provider_outcome; +mod internal_factor_instances_provider_outcome_for_factor; + +pub use factor_instances_provider_outcome::*; +pub use factor_instances_provider_outcome_for_factor::*; +pub use internal_factor_instances_provider_outcome::*; +pub use internal_factor_instances_provider_outcome_for_factor::*; diff --git a/src/factor_instances_provider/provider/provider_adopters/mod.rs b/src/factor_instances_provider/provider/provider_adopters/mod.rs new file mode 100644 index 00000000..2b0676ad --- /dev/null +++ b/src/factor_instances_provider/provider/provider_adopters/mod.rs @@ -0,0 +1,5 @@ +mod securify_entity_factor_instances_provider; +mod virtual_entity_creating_instance_provider; + +pub use securify_entity_factor_instances_provider::*; +pub use virtual_entity_creating_instance_provider::*; diff --git a/src/factor_instances_provider/provider/provider_adopters/securify_entity_factor_instances_provider.rs b/src/factor_instances_provider/provider/provider_adopters/securify_entity_factor_instances_provider.rs new file mode 100644 index 00000000..253010d7 --- /dev/null +++ b/src/factor_instances_provider/provider/provider_adopters/securify_entity_factor_instances_provider.rs @@ -0,0 +1,244 @@ +use crate::prelude::*; + +pub struct SecurifyEntityFactorInstancesProvider; +impl SecurifyEntityFactorInstancesProvider { + /// Reads FactorInstances for every `factor_source` in matrix_of_factor_sources + /// on `network_id` of kind `account_mfa`, + /// meaning `(EntityKind::Account, KeyKind::TransactionSigning, KeySpace::Securified)`, + /// from cache, if any, otherwise derives more of that kind AND other kinds: + /// identity_veci, account_veci, identity_mfa + /// and saves into the cache and returns a collection of instances, per factor source, + /// split into factor instance to use directly and factor instances which was cached, into + /// the mutable `cache` parameter. + /// + /// We are always reading from the beginning of each FactorInstance collection in the cache, + /// and we are always appending to the end. + pub async fn for_account_mfa( + cache: &mut FactorInstancesCache, + profile: Profile, + matrix_of_factor_sources: MatrixOfFactorSources, + account_addresses: IndexSet, + interactors: Arc, + ) -> Result { + Self::for_entity_mfa::( + cache, + profile, + matrix_of_factor_sources, + account_addresses, + interactors, + ) + .await + } + + /// Reads FactorInstances for every `factor_source` in matrix_of_factor_sources + /// on `network_id` of kind `identity_mfa`, + /// meaning `(EntityKind::Identity, KeyKind::TransactionSigning, KeySpace::Securified)`, + /// from cache, if any, otherwise derives more of that kind AND other kinds: + /// identity_veci, account_veci, account_mfa + /// and saves into the cache and returns a collection of instances, per factor source, + /// split into factor instance to use directly and factor instances which was cached, into + /// the mutable `cache` parameter. + /// + /// We are always reading from the beginning of each FactorInstance collection in the cache, + /// and we are always appending to the end. + pub async fn for_persona_mfa( + cache: &mut FactorInstancesCache, + profile: Profile, + matrix_of_factor_sources: MatrixOfFactorSources, + persona_addresses: IndexSet, + interactors: Arc, + ) -> Result { + Self::for_entity_mfa::( + cache, + profile, + matrix_of_factor_sources, + persona_addresses, + interactors, + ) + .await + } + + /// Reads FactorInstances for every `factor_source` in matrix_of_factor_sources + /// on `network_id` of kind `account_mfa` or `identity_mfa` depending on Entity kind, + /// meaning `(EntityKind::_, KeyKind::TransactionSigning, KeySpace::Securified)`, + /// from cache, if any, otherwise derives more of that kind AND other kinds: + /// identity_veci, account_veci, identity_mfa/account_mfa + /// and saves into the cache and returns a collection of instances, per factor source, + /// split into factor instance to use directly and factor instances which was cached, into + /// the mutable `cache` parameter. + /// + /// We are always reading from the beginning of each FactorInstance collection in the cache, + /// and we are always appending to the end. + pub async fn for_entity_mfa( + cache: &mut FactorInstancesCache, + profile: Profile, + matrix_of_factor_sources: MatrixOfFactorSources, + addresses_of_entities: IndexSet, + interactors: Arc, + ) -> Result { + let factor_sources_to_use = matrix_of_factor_sources.all_factors(); + let factor_sources = profile.factor_sources.clone(); + assert!( + factor_sources.is_superset(&factor_sources_to_use), + "Missing FactorSources" + ); + assert!(!addresses_of_entities.is_empty(), "No entities"); + assert!( + addresses_of_entities + .iter() + .all(|e| profile.contains_entity_by_address::(e)), + "unknown entity" + ); + let network_id = addresses_of_entities.first().unwrap().network_id(); + assert!( + addresses_of_entities + .iter() + .all(|a| a.network_id() == network_id), + "wrong network" + ); + + let entity_kind = E::kind(); + + let provider = FactorInstancesProvider::new( + network_id, + factor_sources_to_use, + profile, + cache, + interactors, + ); + + let outcome = provider + .provide(QuantifiedDerivationPreset::new( + DerivationPreset::mfa_entity_kind(entity_kind), + addresses_of_entities.len(), + )) + .await?; + + Ok(outcome.into()) + } +} + +#[cfg(test)] +mod tests { + + use std::ops::Add; + + use crate::{factor_instances_provider::provider::test_sargon_os::SargonOS, prelude::*}; + + type Sut = SecurifyEntityFactorInstancesProvider; + + #[should_panic] + #[actix_rt::test] + async fn mfa_panics_if_entities_empty() { + let fs = HDFactorSource::fs0(); + let a = Account::sample_unsecurified(); + let _ = Sut::for_account_mfa( + &mut FactorInstancesCache::default(), + Profile::new([fs.clone()], [&a], []), + MatrixOfFactorSources::new([], 1, [fs.clone()]), + IndexSet::new(), // <---- EMPTY => should_panic + Arc::new(TestDerivationInteractors::default()), + ) + .await + .unwrap(); + } + + #[should_panic] + #[actix_rt::test] + async fn mfa_panics_if_entity_unknown() { + let fs = HDFactorSource::fs0(); + let a = Account::sample_unsecurified(); + let _ = Sut::for_account_mfa( + &mut FactorInstancesCache::default(), + Profile::new([fs.clone()], [&a], []), + MatrixOfFactorSources::new([], 1, [fs.clone()]), + IndexSet::just(Account::a1().entity_address()), // <---- unknown => should_panic + Arc::new(TestDerivationInteractors::default()), + ) + .await + .unwrap(); + } + + #[should_panic] + #[actix_rt::test] + async fn mfa_panics_if_wrong_network() { + let fs = HDFactorSource::fs0(); + let network = NetworkID::Mainnet; + let mainnet_account = Account::unsecurified_on_network( + "main", + network, + HierarchicalDeterministicFactorInstance::tx_on_network( + CAP26EntityKind::Account, + network, + HDPathComponent::unsecurified_hardening_base_index(0), + fs.factor_source_id(), + ), + ); + let network = NetworkID::Stokenet; + let stokenet_account = Account::unsecurified_on_network( + "stoknet", + network, + HierarchicalDeterministicFactorInstance::tx_on_network( + CAP26EntityKind::Account, + network, + HDPathComponent::unsecurified_hardening_base_index(0), + fs.factor_source_id(), + ), + ); + let profile = Profile::new([fs.clone()], [&mainnet_account, &stokenet_account], []); + assert_eq!(profile.networks.len(), 2); + let _ = Sut::for_account_mfa( + &mut FactorInstancesCache::default(), + profile, + MatrixOfFactorSources::new([], 1, [fs.clone()]), + IndexSet::from_iter([ + mainnet_account.entity_address(), + stokenet_account.entity_address(), + ]), // <---- wrong network => should_panic + Arc::new(TestDerivationInteractors::default()), + ) + .await + .unwrap(); + } + + #[actix_rt::test] + async fn securify_accounts_and_personas_with_override_factor() { + // this is mostly a soundness test for the two functions `for_persona_mfa` and `for_account_mfa` + // using `os` because I'm lazy. We might in fact remove `for_persona_mfa` and `for_account_mfa` + // and only use the `for_entity_mfa` function... but we have these to get code coverage. + let (mut os, bdfs) = SargonOS::with_bdfs().await; + + let (batman, stats) = os.new_mainnet_persona_with_bdfs("Batman").await.unwrap(); + assert!(stats.debug_was_derived.is_empty()); + + let (alice, stats) = os.new_mainnet_account_with_bdfs("alice").await.unwrap(); + assert!(stats.debug_was_derived.is_empty()); + + let shield_0 = MatrixOfFactorSources::new([], 0, [bdfs.clone()]); + let mut cache = os.cache_snapshot(); + let interactors = Arc::new(TestDerivationInteractors::default()); + let outcome = Sut::for_account_mfa( + &mut cache, + os.profile_snapshot(), + shield_0.clone(), + IndexSet::just(alice.entity_address()), + interactors.clone(), + ) + .await + .unwrap(); + let outcome = outcome.per_factor.get(&bdfs.factor_source_id()).unwrap(); + assert_eq!(outcome.to_use_directly.len(), 1); + + let outcome = Sut::for_persona_mfa( + &mut cache, + os.profile_snapshot(), + shield_0.clone(), + IndexSet::just(batman.entity_address()), + interactors.clone(), + ) + .await + .unwrap(); + let outcome = outcome.per_factor.get(&bdfs.factor_source_id()).unwrap(); + assert_eq!(outcome.to_use_directly.len(), 1); + } +} diff --git a/src/factor_instances_provider/provider/provider_adopters/virtual_entity_creating_instance_provider.rs b/src/factor_instances_provider/provider/provider_adopters/virtual_entity_creating_instance_provider.rs new file mode 100644 index 00000000..4d132752 --- /dev/null +++ b/src/factor_instances_provider/provider/provider_adopters/virtual_entity_creating_instance_provider.rs @@ -0,0 +1,495 @@ +use crate::prelude::*; + +/// Uses a `FactorInstancesProvider` provide a VECI for a new virtual entity. +pub struct VirtualEntityCreatingInstanceProvider; +impl VirtualEntityCreatingInstanceProvider { + /// Uses a `FactorInstancesProvider` to provide a VECI for a new virtual entity. + /// + /// Reads FactorInstances for `factor_source` on `network_id` of kind `account_veci`, + /// meaning `(EntityKind::Account, KeyKind::TransactionSigning, KeySpace::Unsecurified)`, + /// from cache, if any, otherwise derives more of that kind AND other kinds: + /// identity_veci, account_mfa, identity_mfa + /// and saves into the cache and returns a collection of instances, split into + /// factor instance to use directly and factor instances which was cached, into + /// the mutable `cache` parameter. + /// + /// We are always reading from the beginning of each FactorInstance collection in the cache, + /// and we are always appending to the end. + pub async fn for_account_veci( + cache: &mut FactorInstancesCache, + profile: Option, + factor_source: HDFactorSource, + network_id: NetworkID, + interactors: Arc, + ) -> Result { + Self::for_entity_veci( + CAP26EntityKind::Account, + cache, + profile, + factor_source, + network_id, + interactors, + ) + .await + } + + /// Reads FactorInstances for `factor_source` on `network_id` of kind `persona_veci`, + /// meaning `(EntityKind::Identity, KeyKind::TransactionSigning, KeySpace::Unsecurified)`, + /// from cache, if any, otherwise derives more of that kind AND other kinds: + /// account_veci, account_mfa, identity_mfa + /// and saves into the cache and returns a collection of instances, split into + /// factor instance to use directly and factor instances which was cached, into + /// the mutable `cache` parameter. + /// + /// We are always reading from the beginning of each FactorInstance collection in the cache, + /// and we are always appending to the end. + pub async fn for_persona_veci( + cache: &mut FactorInstancesCache, + profile: Option, + factor_source: HDFactorSource, + network_id: NetworkID, + interactors: Arc, + ) -> Result { + Self::for_entity_veci( + CAP26EntityKind::Identity, + cache, + profile, + factor_source, + network_id, + interactors, + ) + .await + } + + /// Reads FactorInstances for `factor_source` on `network_id` of kind `account_veci`, + /// meaning `(EntityKind::Account, KeyKind::TransactionSigning, KeySpace::Unsecurified)`, + /// from cache, if any, otherwise derives more of that kind AND other kinds: + /// identity_veci, account_mfa, identity_mfa + /// and saves into the cache and returns a collection of instances, split into + /// factor instance to use directly and factor instances which was cached, into + /// the mutable `cache` parameter. + /// + /// We are always reading from the beginning of each FactorInstance collection in the cache, + /// and we are always appending to the end. + pub async fn for_entity_veci( + entity_kind: CAP26EntityKind, + cache: &mut FactorInstancesCache, + profile: Option, + factor_source: HDFactorSource, + network_id: NetworkID, + interactors: Arc, + ) -> Result { + let provider = FactorInstancesProvider::new( + network_id, + IndexSet::just(factor_source.clone()), + profile, + cache, + interactors, + ); + let outcome = provider + .provide(QuantifiedDerivationPreset::new( + DerivationPreset::veci_entity_kind(entity_kind), + 1, + )) + .await?; + + let outcome = outcome + .per_factor + .get(&factor_source.factor_source_id()) + .cloned() + .expect("Expected to have instances for the factor source"); + + Ok(outcome.into()) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + type Sut = VirtualEntityCreatingInstanceProvider; + + #[actix_rt::test] + async fn cache_is_always_filled_persona_veci_then_after_all_used_we_start_over_at_zero_if_no_profile_is_used( + ) { + let network = NetworkID::Mainnet; + let bdfs = HDFactorSource::sample(); + let mut cache = FactorInstancesCache::default(); + + let outcome = Sut::for_persona_veci( + &mut cache, + None, + bdfs.clone(), + network, + Arc::new(TestDerivationInteractors::default()), + ) + .await + .unwrap(); + + assert_eq!(outcome.factor_source_id, bdfs.factor_source_id()); + + assert_eq!(outcome.debug_found_in_cache.len(), 0); + + assert_eq!( + outcome.debug_was_cached.len(), + DerivationPreset::all().len() * CACHE_FILLING_QUANTITY + ); + + assert_eq!( + outcome.debug_was_derived.len(), + DerivationPreset::all().len() * CACHE_FILLING_QUANTITY + 1 + ); + + let instances_used_directly = outcome.to_use_directly.factor_instances(); + assert_eq!(instances_used_directly.len(), 1); + let instances_used_directly = instances_used_directly.first().unwrap(); + + assert_eq!( + instances_used_directly.derivation_entity_index(), + HDPathComponent::Hardened(HDPathComponentHardened::Unsecurified( + UnsecurifiedIndex::unsecurified_hardening_base_index(0) + )) + ); + + cache.assert_is_full(network, bdfs.factor_source_id()); + + let cached = cache + .peek_all_instances_of_factor_source(bdfs.factor_source_id()) + .unwrap(); + + let persona_veci_paths = cached + .clone() + .get(&DerivationPreset::IdentityVeci.index_agnostic_path_on_network(network)) + .unwrap() + .factor_instances() + .into_iter() + .map(|x| x.derivation_path()) + .collect_vec(); + + assert_eq!(persona_veci_paths.len(), CACHE_FILLING_QUANTITY); + + assert!(persona_veci_paths + .iter() + .all(|x| x.entity_kind == CAP26EntityKind::Identity + && x.network_id == network + && x.key_space() == KeySpace::Unsecurified + && x.key_kind == CAP26KeyKind::TransactionSigning)); + + let persona_veci_indices = persona_veci_paths + .into_iter() + .map(|x| x.index) + .collect_vec(); + + assert_eq!( + persona_veci_indices.first().unwrap().clone(), + HDPathComponent::unsecurified_hardening_base_index(1) + ); + + assert_eq!( + persona_veci_indices.last().unwrap().clone(), + HDPathComponent::unsecurified_hardening_base_index(30) + ); + } + + #[actix_rt::test] + async fn cache_is_always_filled_account_veci_then_after_all_used_we_start_over_at_zero_if_no_profile_is_used( + ) { + let network = NetworkID::Mainnet; + let bdfs = HDFactorSource::sample(); + let mut cache = FactorInstancesCache::default(); + + let outcome = Sut::for_account_veci( + &mut cache, + None, + bdfs.clone(), + network, + Arc::new(TestDerivationInteractors::default()), + ) + .await + .unwrap(); + + assert_eq!(outcome.factor_source_id, bdfs.factor_source_id()); + + assert_eq!(outcome.debug_found_in_cache.len(), 0); + + assert_eq!( + outcome.debug_was_cached.len(), + DerivationPreset::all().len() * CACHE_FILLING_QUANTITY + ); + + assert_eq!( + outcome.debug_was_derived.len(), + DerivationPreset::all().len() * CACHE_FILLING_QUANTITY + 1 + ); + + let instances_used_directly = outcome.to_use_directly.factor_instances(); + assert_eq!(instances_used_directly.len(), 1); + let instances_used_directly = instances_used_directly.first().unwrap(); + + assert_eq!( + instances_used_directly.derivation_entity_index(), + HDPathComponent::Hardened(HDPathComponentHardened::Unsecurified( + UnsecurifiedIndex::unsecurified_hardening_base_index(0) + )) + ); + + cache.assert_is_full(network, bdfs.factor_source_id()); + + let cached = cache + .peek_all_instances_of_factor_source(bdfs.factor_source_id()) + .unwrap(); + + let account_veci_paths = cached + .clone() + .get(&DerivationPreset::AccountVeci.index_agnostic_path_on_network(network)) + .unwrap() + .factor_instances() + .into_iter() + .map(|x| x.derivation_path()) + .collect_vec(); + + assert_eq!(account_veci_paths.len(), CACHE_FILLING_QUANTITY); + + assert!(account_veci_paths + .iter() + .all(|x| x.entity_kind == CAP26EntityKind::Account + && x.network_id == network + && x.key_space() == KeySpace::Unsecurified + && x.key_kind == CAP26KeyKind::TransactionSigning)); + + let account_veci_indices = account_veci_paths + .into_iter() + .map(|x| x.index) + .collect_vec(); + + assert_eq!( + account_veci_indices.first().unwrap().clone(), + HDPathComponent::unsecurified_hardening_base_index(1) + ); + + assert_eq!( + account_veci_indices.last().unwrap().clone(), + HDPathComponent::unsecurified_hardening_base_index(30) + ); + + let account_mfa_paths = cached + .clone() + .get(&DerivationPreset::AccountMfa.index_agnostic_path_on_network(network)) + .unwrap() + .factor_instances() + .into_iter() + .map(|x| x.derivation_path()) + .collect_vec(); + + assert!(account_mfa_paths + .iter() + .all(|x| x.entity_kind == CAP26EntityKind::Account + && x.network_id == network + && x.key_space() == KeySpace::Securified + && x.key_kind == CAP26KeyKind::TransactionSigning)); + + let account_mfa_indices = account_mfa_paths.into_iter().map(|x| x.index).collect_vec(); + + assert_eq!( + account_mfa_indices.first().unwrap().clone(), + HDPathComponent::securifying_base_index(0) + ); + + assert_eq!( + account_mfa_indices.last().unwrap().clone(), + HDPathComponent::securifying_base_index(29) + ); + + let identity_mfa_paths = cached + .clone() + .get(&DerivationPreset::IdentityMfa.index_agnostic_path_on_network(network)) + .unwrap() + .factor_instances() + .into_iter() + .map(|x| x.derivation_path()) + .collect_vec(); + + assert!(identity_mfa_paths + .iter() + .all(|x| x.entity_kind == CAP26EntityKind::Identity + && x.network_id == network + && x.key_space() == KeySpace::Securified + && x.key_kind == CAP26KeyKind::TransactionSigning)); + + let identity_mfa_indices = identity_mfa_paths + .into_iter() + .map(|x| x.index) + .collect_vec(); + + assert_eq!( + identity_mfa_indices.first().unwrap().clone(), + HDPathComponent::securifying_base_index(0) + ); + + assert_eq!( + identity_mfa_indices.last().unwrap().clone(), + HDPathComponent::securifying_base_index(29) + ); + + let identity_veci_paths = cached + .clone() + .get(&DerivationPreset::IdentityVeci.index_agnostic_path_on_network(network)) + .unwrap() + .factor_instances() + .into_iter() + .map(|x| x.derivation_path()) + .collect_vec(); + + assert!(identity_veci_paths + .iter() + .all(|x| x.entity_kind == CAP26EntityKind::Identity + && x.network_id == network + && x.key_space() == KeySpace::Unsecurified + && x.key_kind == CAP26KeyKind::TransactionSigning)); + + let identity_veci_indices = identity_veci_paths + .into_iter() + .map(|x| x.index) + .collect_vec(); + + assert_eq!( + identity_veci_indices.first().unwrap().clone(), + HDPathComponent::unsecurified_hardening_base_index(0) + ); + + assert_eq!( + identity_veci_indices.last().unwrap().clone(), + HDPathComponent::unsecurified_hardening_base_index(29) + ); + + // lets create another account (same network, same factor source) + + let outcome = Sut::for_account_veci( + &mut cache, + None, + bdfs.clone(), + network, + Arc::new(TestDerivationInteractors::default()), + ) + .await + .unwrap(); + + assert_eq!(outcome.factor_source_id, bdfs.factor_source_id()); + assert_eq!(outcome.debug_found_in_cache.len(), 1); // This time we found in cache + assert_eq!(outcome.debug_was_cached.len(), 0); + assert_eq!(outcome.debug_was_derived.len(), 0); + + let instances_used_directly = outcome.to_use_directly.factor_instances(); + assert_eq!(instances_used_directly.len(), 1); + let instances_used_directly = instances_used_directly.first().unwrap(); + + assert_eq!( + instances_used_directly.derivation_entity_index(), + HDPathComponent::Hardened(HDPathComponentHardened::Unsecurified( + UnsecurifiedIndex::unsecurified_hardening_base_index(1) // Next one! + )) + ); + + assert!(!cache.is_full(network, bdfs.factor_source_id())); // not full anymore, since we just used a veci + + let cached = cache + .peek_all_instances_of_factor_source(bdfs.factor_source_id()) + .unwrap(); + + let account_veci_paths = cached + .clone() + .get(&DerivationPreset::AccountVeci.index_agnostic_path_on_network(network)) + .unwrap() + .factor_instances() + .into_iter() + .map(|x| x.derivation_path()) + .collect_vec(); + + assert_eq!(account_veci_paths.len(), CACHE_FILLING_QUANTITY - 1); + + assert!(account_veci_paths + .iter() + .all(|x| x.entity_kind == CAP26EntityKind::Account + && x.network_id == network + && x.key_space() == KeySpace::Unsecurified + && x.key_kind == CAP26KeyKind::TransactionSigning)); + + let account_veci_indices = account_veci_paths + .into_iter() + .map(|x| x.index) + .collect_vec(); + + assert_eq!( + account_veci_indices.first().unwrap().clone(), + HDPathComponent::unsecurified_hardening_base_index(2) // first is not `1` anymore + ); + + assert_eq!( + account_veci_indices.last().unwrap().clone(), + HDPathComponent::unsecurified_hardening_base_index(30) + ); + + // create 29 more accounts, then we should be able to crate one more which should ONLY derive + // more instances for ACCOUNT VECI, and not Identity Veci, Identity MFA and Account MFA, since that is + // not needed. + for _ in 0..29 { + let outcome = Sut::for_account_veci( + &mut cache, + None, + bdfs.clone(), + network, + Arc::new(TestDerivationInteractors::default()), + ) + .await + .unwrap(); + + assert_eq!(outcome.factor_source_id, bdfs.factor_source_id()); + + assert_eq!(outcome.debug_found_in_cache.len(), 1); + assert_eq!(outcome.debug_was_cached.len(), 0); + assert_eq!(outcome.debug_was_derived.len(), 0); + } + + let cached = cache + .peek_all_instances_of_factor_source(bdfs.factor_source_id()) + .unwrap(); + + assert!( + cached + .get(&DerivationPreset::AccountVeci.index_agnostic_path_on_network(network)) + .is_none(), + "should have used the last instance..." + ); + + // Great, now lets create one more account, and this time we should derive more instances for + // it. We should derive 31 instances, 30 for account veci to cache and 1 to use directly. + // we should NOT derive more instances for Identity Veci, Identity MFA and Account MFA, since + // that cache is already full. + let outcome = Sut::for_account_veci( + &mut cache, + None, + bdfs.clone(), + network, + Arc::new(TestDerivationInteractors::default()), + ) + .await + .unwrap(); + + assert_eq!(outcome.factor_source_id, bdfs.factor_source_id()); + + assert_eq!(outcome.debug_found_in_cache.len(), 0); + assert_eq!(outcome.debug_was_cached.len(), CACHE_FILLING_QUANTITY); // ONLY 30, not 120... + assert_eq!(outcome.debug_was_derived.len(), CACHE_FILLING_QUANTITY + 1); + + let instances_used_directly = outcome.to_use_directly.factor_instances(); + assert_eq!(instances_used_directly.len(), 1); + let instances_used_directly = instances_used_directly.first().unwrap(); + + assert_eq!( + instances_used_directly.derivation_entity_index(), + HDPathComponent::Hardened(HDPathComponentHardened::Unsecurified( + UnsecurifiedIndex::unsecurified_hardening_base_index(0) // IMPORTANT! Index 0 is used again! Why?! Well because are not using a Profile here, and we are not eagerly filling cache just before we are using the last index. + )) + ); + } +} diff --git a/src/factor_instances_provider/provider/test_sargon_os.rs b/src/factor_instances_provider/provider/test_sargon_os.rs new file mode 100644 index 00000000..d0d2ae76 --- /dev/null +++ b/src/factor_instances_provider/provider/test_sargon_os.rs @@ -0,0 +1,349 @@ +#![cfg(test)] + +use crate::prelude::*; + +/// Should be merged with SargonOS in Sargon repo... +/// contains three fundamentally new methods: +/// * new_account (using `FactorInstancesProvider`) +/// * new_persona (using `FactorInstancesProvider`) +/// * securify_accounts (using `FactorInstancesProvider`) +/// * add_factor_source (using `FactorInstancesProvider`) +pub(super) struct SargonOS { + /// FactorInstancesCache of prederived FactorInstances for each factor source in Profile. + pub(super) cache: FactorInstancesCache, + profile: RwLock, +} + +impl SargonOS { + pub(super) fn profile_snapshot(&self) -> Profile { + self.profile.try_read().unwrap().clone() + } + + pub(super) fn new() -> Self { + Arc::new(TestDerivationInteractors::default()); + Self { + cache: FactorInstancesCache::default(), + profile: RwLock::new(Profile::default()), + } + } + + pub(super) async fn with_bdfs() -> (Self, HDFactorSource) { + let mut self_ = Self::new(); + let bdfs = HDFactorSource::device(); + self_.add_factor_source(bdfs.clone()).await.unwrap(); + (self_, bdfs) + } + + pub(super) fn cache_snapshot(&self) -> FactorInstancesCache { + self.cache.clone() + } + + pub(super) fn clear_cache(&mut self) { + println!("💣 CLEAR CACHE"); + self.cache = FactorInstancesCache::default() + } + + pub(super) async fn new_mainnet_account_with_bdfs( + &mut self, + name: impl AsRef, + ) -> Result<(Account, FactorInstancesProviderOutcomeForFactor)> { + self.new_account_with_bdfs(NetworkID::Mainnet, name).await + } + + pub(super) async fn new_account_with_bdfs( + &mut self, + network: NetworkID, + name: impl AsRef, + ) -> Result<(Account, FactorInstancesProviderOutcomeForFactor)> { + let bdfs = self.profile_snapshot().bdfs(); + self.new_account(bdfs, network, name).await + } + + pub(super) async fn new_account( + &mut self, + factor_source: HDFactorSource, + network: NetworkID, + name: impl AsRef, + ) -> Result<(Account, FactorInstancesProviderOutcomeForFactor)> { + self.new_entity(factor_source, network, name).await + } + + pub(super) async fn new_mainnet_persona_with_bdfs( + &mut self, + name: impl AsRef, + ) -> Result<(Persona, FactorInstancesProviderOutcomeForFactor)> { + self.new_persona_with_bdfs(NetworkID::Mainnet, name).await + } + + pub(super) async fn new_persona_with_bdfs( + &mut self, + network: NetworkID, + name: impl AsRef, + ) -> Result<(Persona, FactorInstancesProviderOutcomeForFactor)> { + let bdfs = self.profile_snapshot().bdfs(); + self.new_persona(bdfs, network, name).await + } + + pub(super) async fn new_persona( + &mut self, + factor_source: HDFactorSource, + network: NetworkID, + name: impl AsRef, + ) -> Result<(Persona, FactorInstancesProviderOutcomeForFactor)> { + self.new_entity(factor_source, network, name).await + } + + pub(super) async fn new_entity( + &mut self, + factor_source: HDFactorSource, + network: NetworkID, + name: impl AsRef, + ) -> Result<(E, FactorInstancesProviderOutcomeForFactor)> { + let profile_snapshot = self.profile_snapshot(); + let outcome = VirtualEntityCreatingInstanceProvider::for_entity_veci( + E::kind(), + &mut self.cache, + Some(profile_snapshot), + factor_source.clone(), + network, + Arc::new(TestDerivationInteractors::default()), + ) + .await + .unwrap(); + + let outcome_for_factor = outcome; + + let instances_to_use_directly = outcome_for_factor.to_use_directly.clone(); + + assert_eq!(instances_to_use_directly.len(), 1); + let instance = instances_to_use_directly.first().unwrap(); + + let address = E::Address::new(network, instance.public_key_hash()); + let security_state = EntitySecurityState::Unsecured(instance); + let entity = E::new( + name, + address, + security_state, + ThirdPartyDepositPreference::default(), + ); + self.profile + .try_write() + .unwrap() + .insert_entities(IndexSet::just(Into::::into( + entity.clone(), + ))) + .unwrap(); + + Ok((entity, outcome_for_factor)) + } + + pub(super) async fn securify_account( + &mut self, + account_addresses: AccountAddress, + shield: MatrixOfFactorSources, + ) -> Result<(SecurifiedAccount, FactorInstancesProviderOutcome)> { + let (accounts, stats) = self + .securify_accounts(IndexSet::just(account_addresses), shield) + .await?; + assert_eq!(accounts.len(), 1); + let account = accounts.into_iter().next().unwrap(); + Ok((account, stats)) + } + + pub(super) async fn securify_accounts( + &mut self, + account_addresses: IndexSet, + shield: MatrixOfFactorSources, + ) -> Result<(SecurifiedAccounts, FactorInstancesProviderOutcome)> { + self.securify_accounts_with_interactor( + Arc::new(TestDerivationInteractors::default()), + account_addresses, + shield, + ) + .await + } + + pub(super) async fn securify_accounts_with_interactor( + &mut self, + interactor: Arc, + account_addresses: IndexSet, + shield: MatrixOfFactorSources, + ) -> Result<(SecurifiedAccounts, FactorInstancesProviderOutcome)> { + assert!(!account_addresses.is_empty()); + let network = account_addresses.first().unwrap().network_id(); + let (entities, stats) = self + .securify_entities_with_interactor::( + interactor, + account_addresses, + shield, + ) + .await?; + Ok((SecurifiedAccounts::new(network, entities).unwrap(), stats)) + } + + pub(super) async fn securify_personas( + &mut self, + identity_addresses: IndexSet, + shield: MatrixOfFactorSources, + ) -> Result<(SecurifiedPersonas, FactorInstancesProviderOutcome)> { + self.securify_personas_with_interactor( + Arc::new(TestDerivationInteractors::default()), + identity_addresses, + shield, + ) + .await + } + + pub(super) async fn securify_personas_with_interactor( + &mut self, + interactor: Arc, + identity_addresses: IndexSet, + shield: MatrixOfFactorSources, + ) -> Result<(SecurifiedPersonas, FactorInstancesProviderOutcome)> { + assert!(!identity_addresses.is_empty()); + let network = identity_addresses.first().unwrap().network_id(); + let (entities, stats) = self + .securify_entities_with_interactor::( + interactor, + identity_addresses, + shield, + ) + .await?; + Ok((SecurifiedPersonas::new(network, entities).unwrap(), stats)) + } + + pub(super) async fn securify_entities_with_interactor( + &mut self, + interactor: Arc, + addresses_of_entities: IndexSet<::Address>, + shield: MatrixOfFactorSources, + ) -> Result<(IndexSet, FactorInstancesProviderOutcome)> { + let profile_snapshot = self.profile_snapshot(); + + let outcome = SecurifyEntityFactorInstancesProvider::for_entity_mfa::( + &mut self.cache, + profile_snapshot.clone(), + shield.clone(), + addresses_of_entities.clone(), + interactor, + ) + .await?; + + let mut instance_per_factor = outcome + .clone() + .per_factor + .into_iter() + .map(|(k, outcome_per_factor)| (k, outcome_per_factor.to_use_directly)) + .collect::>(); + + assert_eq!( + instance_per_factor + .keys() + .cloned() + .collect::>(), + shield + .all_factors() + .into_iter() + .map(|f| f.factor_source_id()) + .collect::>() + ); + + // Now we need to map the flat set of instances into many MatrixOfFactorInstances, and assign + // one to each account + let updated_entities = addresses_of_entities + .clone() + .into_iter() + .map(|a| { + let entity = profile_snapshot.get_entity::(&a).unwrap(); + let matrix_of_instances = + MatrixOfFactorInstances::fulfilling_matrix_of_factor_sources_with_instances( + &mut instance_per_factor, + shield.clone(), + ) + .unwrap(); + + let access_controller = match entity.security_state() { + EntitySecurityState::Unsecured(_) => { + AccessController::from_unsecurified_address(a) + } + EntitySecurityState::Securified(sec) => sec.access_controller.clone(), + }; + let veci = match entity.security_state() { + EntitySecurityState::Unsecured(veci) => Some(veci), + EntitySecurityState::Securified(sec) => { + sec.veci.clone().map(|x| x.factor_instance()) + } + }; + let sec = SecurifiedEntityControl::new( + matrix_of_instances, + access_controller, + veci.map(|x| VirtualEntityCreatingInstance::new(x, entity.address())), + ); + + E::new( + entity.name(), + entity.entity_address(), + sec, + entity.third_party_deposit(), + ) + }) + .collect::>(); + + for entity in updated_entities.clone().into_iter() { + self.profile + .try_write() + .unwrap() + .update_entity::(entity.into()) + } + assert!( + instance_per_factor.values().all(|x| x.is_empty()), + "should have used all instances, but have unused instances: {:?}", + instance_per_factor + ); + + Ok((updated_entities, outcome)) + } + + /// Pre-Derives FactorInstances and saves them into the cache + pub(super) async fn add_factor_source(&mut self, factor_source: HDFactorSource) -> Result<()> { + let profile_snapshot = self.profile_snapshot(); + assert!( + !profile_snapshot + .factor_sources + .iter() + .any(|x| x.factor_source_id() == factor_source.factor_source_id()), + "factor already in Profile" + ); + let outcome = CacheFiller::for_new_factor_source( + &mut self.cache, + Some(profile_snapshot), + factor_source.clone(), + NetworkID::Mainnet, + Arc::new(TestDerivationInteractors::default()), + ) + .await + .unwrap(); + + assert_eq!(outcome.factor_source_id, factor_source.factor_source_id()); + + assert_eq!(outcome.debug_found_in_cache.len(), 0); + + assert_eq!( + outcome.debug_was_cached.len(), + DerivationPreset::all().len() * CACHE_FILLING_QUANTITY + ); + + assert_eq!( + outcome.debug_was_derived.len(), + DerivationPreset::all().len() * CACHE_FILLING_QUANTITY + ); + + self.profile + .try_write() + .unwrap() + .add_factor_source(factor_source.clone()) + .unwrap(); + + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 8009ebc4..779e635b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ #![feature(step_trait)] mod derivation; +mod factor_instances_provider; mod gateway; mod samples; mod signing; @@ -20,6 +21,7 @@ pub mod prelude { pub(crate) use crate::samples::*; + pub use crate::factor_instances_provider::*; pub use crate::signing::*; pub use crate::types::*; diff --git a/src/types/new_methods_on_sargon_types.rs b/src/types/new_methods_on_sargon_types.rs index 7674e3c9..22bb7ba9 100644 --- a/src/types/new_methods_on_sargon_types.rs +++ b/src/types/new_methods_on_sargon_types.rs @@ -22,21 +22,6 @@ impl TransactionIntent { } } -#[derive(Clone, Copy, PartialEq, Eq, Hash, derive_more::Display, derive_more::Debug)] -pub enum KeySpace { - #[display("Unsecurified")] - #[debug("Unsecurified")] - Unsecurified, - #[display("Securified")] - #[debug("Securified")] - Securified, -} -impl KeySpace { - pub fn both() -> [Self; 2] { - [Self::Unsecurified, Self::Securified] - } -} - impl DerivationPath { pub fn key_space(&self) -> KeySpace { self.index.key_space() diff --git a/src/types/new_types/accounts.rs b/src/types/new_types/accounts.rs index 34a847f1..843eb50c 100644 --- a/src/types/new_types/accounts.rs +++ b/src/types/new_types/accounts.rs @@ -1,51 +1,52 @@ use crate::prelude::*; -/// A NonEmpty collection of Accounts all on the SAME Network +pub type Accounts = Entities; +pub type Personas = Entities; + +/// A NonEmpty collection of Entities all on the SAME Network /// but mixed if they are securified or unsecurified. #[derive(Clone, Debug, PartialEq, Eq)] -pub struct Accounts { +pub struct Entities { pub network_id: NetworkID, - accounts: IndexSet, + entities: IndexSet, } -impl Accounts { - pub fn just(account: Account) -> Self { - Self { - network_id: account.network_id(), - accounts: IndexSet::just(account), - } - } - pub fn new(network_id: NetworkID, accounts: IndexSet) -> Result { - if accounts.is_empty() { + +impl Entities { + pub fn new(network_id: NetworkID, entities: IndexSet) -> Result { + if entities.is_empty() { return Err(CommonError::EmptyCollection); } - if !accounts.iter().all(|a| a.network_id() == network_id) { + if !entities.iter().all(|a| a.network_id() == network_id) { return Err(CommonError::WrongNetwork); } Ok(Self { network_id, - accounts, + entities, }) } -} -impl IntoIterator for Accounts { - type Item = Account; - type IntoIter = as IntoIterator>::IntoIter; - fn into_iter(self) -> Self::IntoIter { - self.accounts.clone().into_iter() - } -} -impl Accounts { + pub fn len(&self) -> usize { - self.accounts.len() + self.entities.len() } + /// Should never be true, since we do not allow empty. pub fn is_empty(&self) -> bool { - self.accounts.is_empty() + self.entities.is_empty() } + pub fn network_id(&self) -> NetworkID { self.network_id } } + +impl IntoIterator for Entities { + type Item = E; + type IntoIter = as IntoIterator>::IntoIter; + fn into_iter(self) -> Self::IntoIter { + self.entities.clone().into_iter() + } +} + #[cfg(test)] mod tests { use super::*; @@ -70,7 +71,7 @@ mod tests { assert!(matches!( Sut::new( NetworkID::Stokenet, - IndexSet::from_iter([Account::sample_other(), Item::sample(),]) + IndexSet::from_iter([Item::sample_other(), Item::sample(),]) ), Err(CommonError::WrongNetwork) )); diff --git a/src/types/new_types/appendable_collection.rs b/src/types/new_types/appendable_collection.rs new file mode 100644 index 00000000..a65ff36f --- /dev/null +++ b/src/types/new_types/appendable_collection.rs @@ -0,0 +1,95 @@ +use crate::prelude::*; +use std::borrow::Borrow; + +pub trait AppendableCollection: FromIterator { + type Element; + fn append>(&mut self, iter: T); +} +impl AppendableCollection for IndexSet { + type Element = V; + + fn append>(&mut self, iter: T) { + self.extend(iter) + } +} + +impl AppendableCollection for FactorInstances { + type Element = HierarchicalDeterministicFactorInstance; + + fn append>(&mut self, iter: T) { + self.extend(iter) + } +} + +pub trait AppendableMap { + type Key: Eq + std::hash::Hash + Clone; + type AC: AppendableCollection; + fn append_or_insert_to::Element>>( + &mut self, + key: impl Borrow, + items: I, + ); + + fn append_or_insert_element_to( + &mut self, + key: impl Borrow, + element: ::Element, + ) { + self.append_or_insert_to(key.borrow(), [element]); + } +} + +impl AppendableCollection for IndexMap +where + K: Eq + std::hash::Hash + Clone, +{ + type Element = (K, V); + + fn append>(&mut self, iter: T) { + self.extend(iter) + } +} + +impl AppendableMap for IndexMap +where + K: Eq + std::hash::Hash + Clone, + V: AppendableCollection, +{ + type Key = K; + type AC = V; + fn append_or_insert_to::Element>>( + &mut self, + key: impl Borrow, + items: I, + ) { + let key = key.borrow(); + if let Some(existing) = self.get_mut(key) { + existing.append(items); + } else { + self.insert(key.clone(), V::from_iter(items)); + } + } +} + +#[cfg(test)] +mod test_appendable_collection { + use super::*; + + #[test] + fn test_append_element() { + type Sut = IndexMap>; + let mut map = Sut::new(); + map.append_or_insert_element_to(-3, 5); + map.append_or_insert_element_to(-3, 6); + map.append_or_insert_element_to(-3, 6); + map.append_or_insert_to(-3, [42, 237]); + map.append_or_insert_to(-9, [64, 128]); + assert_eq!( + map, + Sut::from_iter([ + (-3, IndexSet::::from_iter([5, 6, 42, 237])), + (-9, IndexSet::::from_iter([64, 128])), + ]) + ); + } +} diff --git a/src/types/new_types/factor_instances.rs b/src/types/new_types/factor_instances.rs index c1e9abd0..078a7cbc 100644 --- a/src/types/new_types/factor_instances.rs +++ b/src/types/new_types/factor_instances.rs @@ -1,12 +1,14 @@ use crate::prelude::*; /// A collection of factor instances. -#[derive(Debug, Default, Clone, PartialEq, Eq)] +#[derive(Default, Clone, PartialEq, Eq, derive_more::Debug)] +#[debug("FIS[{:?}]", self.factor_instances)] pub struct FactorInstances { #[allow(dead_code)] hidden: HiddenConstructor, factor_instances: IndexSet, } + impl FactorInstances { pub fn extend( &mut self, @@ -23,8 +25,19 @@ impl FactorInstances { pub fn first(&self) -> Option { self.factor_instances.first().cloned() } + pub fn split_at(self, mid: usize) -> (Self, Self) { + let instances = self.factor_instances.into_iter().collect_vec(); + let (head, tail) = instances.split_at(mid); + (Self::from(head), Self::from(tail)) + } +} +impl From<&[HierarchicalDeterministicFactorInstance]> for FactorInstances { + fn from(value: &[HierarchicalDeterministicFactorInstance]) -> Self { + Self::from( + IndexSet::::from_iter(value.iter().cloned()), + ) + } } - impl From> for FactorInstances { fn from(instances: IndexSet) -> Self { Self::new(instances) @@ -76,6 +89,11 @@ impl FactorInstances { factor_instances, } } + + pub fn just(factor_instance: HierarchicalDeterministicFactorInstance) -> Self { + Self::new(IndexSet::just(factor_instance)) + } + pub fn factor_instances(&self) -> IndexSet { self.factor_instances.clone() } diff --git a/src/types/new_types/is_securified_entity.rs b/src/types/new_types/is_securified_entity.rs new file mode 100644 index 00000000..bd98327e --- /dev/null +++ b/src/types/new_types/is_securified_entity.rs @@ -0,0 +1,71 @@ +use std::hash::Hash; + +use crate::prelude::*; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct AssertMatches { + pub network_id: NetworkID, + pub key_kind: CAP26KeyKind, + pub entity_kind: CAP26EntityKind, + pub key_space: KeySpace, +} +impl AssertMatches { + pub fn matches(&self, path: &DerivationPath) -> DerivationPath { + assert_eq!(self.entity_kind, path.entity_kind); + assert_eq!(self.network_id, path.network_id); + assert_eq!(self.entity_kind, path.entity_kind); + assert_eq!(self.key_space, path.key_space()); + path.clone() + } +} +impl MatrixOfFactorInstances { + fn highest_derivation_path_index( + &self, + factor_source_id: FactorSourceIDFromHash, + assert_matches: AssertMatches, + ) -> Option { + self.all_factors() + .into_iter() + .filter(|f| f.factor_source_id() == factor_source_id) + .map(|f| f.derivation_path()) + .map(|p| assert_matches.matches(&p)) + .map(|p| p.index) + .max() + } +} +impl SecurifiedEntityControl { + fn highest_derivation_path_index( + &self, + factor_source_id: FactorSourceIDFromHash, + assert_matches: AssertMatches, + ) -> Option { + self.matrix + .highest_derivation_path_index(factor_source_id, assert_matches) + } +} + +pub trait IsSecurifiedEntity: + Hash + Eq + Clone + IsNetworkAware + TryFrom + Into +{ + type BaseEntity: IsEntity + std::hash::Hash + Eq; + fn kind() -> CAP26EntityKind { + Self::BaseEntity::kind() + } + fn securified_entity_control(&self) -> SecurifiedEntityControl; + + fn new( + name: impl AsRef, + address: ::Address, + securified_entity_control: SecurifiedEntityControl, + third_party_deposit: impl Into>, + ) -> Self; + + fn highest_derivation_path_index( + &self, + factor_source_id: FactorSourceIDFromHash, + assert_matches: AssertMatches, + ) -> Option { + self.securified_entity_control() + .highest_derivation_path_index(factor_source_id, assert_matches) + } +} diff --git a/src/types/new_types/key_space.rs b/src/types/new_types/key_space.rs new file mode 100644 index 00000000..aec97c4c --- /dev/null +++ b/src/types/new_types/key_space.rs @@ -0,0 +1,44 @@ +use crate::prelude::*; + +/// We split the hardened derivation entity index "space" in +/// two halves. The first half is used for unsecurified entities, +/// and the second half is used for securified entities. +/// +/// The Unsecurified half works as it does today, with hardened +/// `u32` values, where hardened denotes addition of `2^31`. +/// +/// The Securified half is a new concept, where we offset the +/// `u32` value with half of the 2^31 space, i.e. `2^30`. +#[derive(Clone, Copy, PartialEq, Eq, Hash, derive_more::Display, derive_more::Debug)] +pub enum KeySpace { + /// Used by FactorInstances controlling + /// unsecurified entities, called "VECI"s + /// Virtual Entity Creating (Factor)Instances. + #[display("Unsecurified")] + #[debug("Unsecurified")] + Unsecurified, + + /// Used by FactorInstances in MatrixOfFactorInstances + /// for securified entities. + /// + /// This is the entity base index value, `u32` `+ 2^30`. + /// + /// We use `6^` notation to indicate: `6' + 2^30`, where `'`, + /// is the standard notation for hardened indices. + #[display("Securified")] + #[debug("Securified")] + Securified, +} + +impl KeySpace { + pub fn both() -> [Self; 2] { + [Self::Unsecurified, Self::Securified] + } + + pub fn indicator(&self) -> String { + match self { + Self::Unsecurified => "'".to_owned(), + Self::Securified => "^".to_owned(), + } + } +} diff --git a/src/types/new_types/mod.rs b/src/types/new_types/mod.rs index d870f962..88f30014 100644 --- a/src/types/new_types/mod.rs +++ b/src/types/new_types/mod.rs @@ -1,17 +1,23 @@ mod accounts; +mod appendable_collection; mod factor_instances; +mod is_securified_entity; +mod key_space; mod securified_account; mod securified_accounts; -mod securified_entities; +mod securified_persona; mod u30; mod unsecurified_entity; mod veci; pub use accounts::*; +pub use appendable_collection::*; pub use factor_instances::*; +pub use is_securified_entity::*; +pub use key_space::*; pub use securified_account::*; pub use securified_accounts::*; -pub use securified_entities::*; +pub use securified_persona::*; pub use u30::*; pub use unsecurified_entity::*; pub use veci::*; diff --git a/src/types/new_types/securified_account.rs b/src/types/new_types/securified_account.rs index 2b06334f..23d052dd 100644 --- a/src/types/new_types/securified_account.rs +++ b/src/types/new_types/securified_account.rs @@ -1,4 +1,5 @@ use crate::prelude::*; + /// The `SecurifiedEntityControl`, address and possibly third party deposit state of some /// Securified entity. #[derive(Clone, Debug, PartialEq, Eq, Hash)] @@ -12,8 +13,18 @@ pub struct SecurifiedAccount { /// settings. third_party_deposit: Option, } -impl SecurifiedAccount { - pub fn new( +impl From for Account { + fn from(value: SecurifiedAccount) -> Account { + value.account() + } +} +impl IsSecurifiedEntity for SecurifiedAccount { + type BaseEntity = Account; + fn securified_entity_control(&self) -> SecurifiedEntityControl { + self.securified_entity_control() + } + + fn new( name: impl AsRef, address: AccountAddress, securified_entity_control: SecurifiedEntityControl, @@ -26,6 +37,67 @@ impl SecurifiedAccount { third_party_deposit: third_party_deposit.into(), } } +} + +impl IsNetworkAware for SecurifiedAccount { + fn network_id(&self) -> NetworkID { + self.address().network_id() + } +} + +impl TryFrom for SecurifiedAccount { + type Error = CommonError; + fn try_from(value: Account) -> Result { + let securified_entity_control = value + .security_state() + .as_securified() + .cloned() + .ok_or(CommonError::AccountNotSecurified)?; + Ok(SecurifiedAccount::new( + value.name(), + value.entity_address(), + securified_entity_control, + value.third_party_deposit(), + )) + } +} + +impl TryFrom for SecurifiedAccount { + type Error = CommonError; + fn try_from(value: AccountOrPersona) -> Result { + Account::try_from(value).and_then(SecurifiedAccount::try_from) + } +} +impl From for Persona { + fn from(value: SecurifiedPersona) -> Persona { + value.persona() + } +} +impl TryFrom for SecurifiedPersona { + type Error = CommonError; + fn try_from(value: Persona) -> Result { + let securified_entity_control = value + .security_state() + .as_securified() + .cloned() + .ok_or(CommonError::PersonaNotSecurified)?; + Ok(SecurifiedPersona::new( + value.name(), + value.entity_address(), + securified_entity_control, + value.third_party_deposit(), + )) + } +} + +impl TryFrom for SecurifiedPersona { + type Error = CommonError; + fn try_from(value: AccountOrPersona) -> Result { + Persona::try_from(value).and_then(SecurifiedPersona::try_from) + } +} + +impl SecurifiedAccount { pub fn account(&self) -> Account { Account::new( self.name.clone(), @@ -37,9 +109,6 @@ impl SecurifiedAccount { pub fn address(&self) -> AccountAddress { self.account_address.clone() } - pub fn network_id(&self) -> NetworkID { - self.address().network_id() - } pub fn securified_entity_control(&self) -> SecurifiedEntityControl { self.securified_entity_control.clone() } @@ -47,6 +116,7 @@ impl SecurifiedAccount { self.third_party_deposit } } + impl HasSampleValues for SecurifiedAccount { fn sample() -> Self { Self::new( @@ -65,6 +135,7 @@ impl HasSampleValues for SecurifiedAccount { ) } } + #[cfg(test)] mod tests { use super::*; diff --git a/src/types/new_types/securified_accounts.rs b/src/types/new_types/securified_accounts.rs index e3ff573a..d53e687e 100644 --- a/src/types/new_types/securified_accounts.rs +++ b/src/types/new_types/securified_accounts.rs @@ -1,41 +1,8 @@ use crate::prelude::*; -/// A NonEmpty collection of Accounts all on the SAME Network and all verified -/// to be Securified. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct SecurifiedAccounts { - network_id: NetworkID, - accounts: IndexSet, -} -impl IntoIterator for SecurifiedAccounts { - type Item = SecurifiedAccount; - type IntoIter = as IntoIterator>::IntoIter; - fn into_iter(self) -> Self::IntoIter { - self.accounts.clone().into_iter() - } -} -impl SecurifiedAccounts { - pub fn new(network_id: NetworkID, accounts: IndexSet) -> Result { - if accounts.is_empty() { - return Err(CommonError::EmptyCollection); - } - if !accounts.iter().all(|a| a.network_id() == network_id) { - return Err(CommonError::WrongNetwork); - } - Ok(Self { - network_id, - accounts, - }) - } - pub fn network_id(&self) -> NetworkID { - self.network_id - } - pub fn len(&self) -> usize { - self.accounts.len() - } - pub fn is_empty(&self) -> bool { - self.accounts.is_empty() - } -} + +pub type SecurifiedAccounts = Entities; +pub type SecurifiedPersonas = Entities; + #[cfg(test)] mod tests { use super::*; diff --git a/src/types/new_types/securified_entities.rs b/src/types/new_types/securified_entities.rs deleted file mode 100644 index 61a59edf..00000000 --- a/src/types/new_types/securified_entities.rs +++ /dev/null @@ -1,123 +0,0 @@ -use crate::prelude::*; - -/// The `SecurifiedEntityControl`, address and possibly third party deposit state of some -/// Securified entity. -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct SecurifiedEntity { - /// The address which is verified to match the `veci` - address: AddressOfAccountOrPersona, - - securified_entity_control: SecurifiedEntityControl, - - /// If we found this UnsecurifiedEntity while scanning OnChain using - /// Gateway, we might have been able to read out the third party deposit - /// settings. - third_party_deposit: Option, -} - -impl SecurifiedEntity { - pub fn network_id(&self) -> NetworkID { - self.address.network_id() - } - pub fn new( - address: AddressOfAccountOrPersona, - securified_entity_control: SecurifiedEntityControl, - third_party_deposit: impl Into>, - ) -> Self { - Self { - address, - securified_entity_control, - third_party_deposit: third_party_deposit.into(), - } - } - - pub fn address(&self) -> AddressOfAccountOrPersona { - self.address.clone() - } - - pub fn securified_entity_control(&self) -> SecurifiedEntityControl { - self.securified_entity_control.clone() - } - - pub fn third_party_deposit(&self) -> Option { - self.third_party_deposit - } -} - -impl HasSampleValues for SecurifiedEntity { - fn sample() -> Self { - Self::new( - AddressOfAccountOrPersona::sample(), - SecurifiedEntityControl::sample(), - ThirdPartyDepositPreference::sample(), - ) - } - fn sample_other() -> Self { - Self::new( - AddressOfAccountOrPersona::sample_other(), - SecurifiedEntityControl::sample_other(), - ThirdPartyDepositPreference::sample_other(), - ) - } -} - -impl From for AccountOrPersona { - fn from(value: SecurifiedEntity) -> Self { - let address = value.address(); - let name = "Recovered"; - let security_state = EntitySecurityState::Securified(value.securified_entity_control()); - - if let Ok(account_address) = address.clone().into_account() { - Account::new( - name, - account_address, - security_state, - value.third_party_deposit(), - ) - .into() - } else if let Ok(identity_address) = address.clone().into_identity() { - Persona::new( - name, - identity_address, - security_state, - value.third_party_deposit(), - ) - .into() - } else { - unreachable!("Either account or persona.") - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - type Sut = SecurifiedEntity; - - #[test] - fn equality() { - assert_eq!(Sut::sample(), Sut::sample()); - assert_eq!(Sut::sample_other(), Sut::sample_other()); - } - - #[test] - fn inequality() { - assert_ne!(Sut::sample(), Sut::sample_other()); - } - - #[test] - fn third_party_dep() { - let test = |dep: ThirdPartyDepositPreference| { - let sut = Sut::new( - AddressOfAccountOrPersona::sample(), - SecurifiedEntityControl::sample(), - dep, - ); - assert_eq!(sut.third_party_deposit(), Some(dep)); - }; - test(ThirdPartyDepositPreference::DenyAll); - test(ThirdPartyDepositPreference::AllowAll); - test(ThirdPartyDepositPreference::AllowKnown); - } -} diff --git a/src/types/new_types/securified_persona.rs b/src/types/new_types/securified_persona.rs new file mode 100644 index 00000000..d04c9b1a --- /dev/null +++ b/src/types/new_types/securified_persona.rs @@ -0,0 +1,92 @@ +use crate::prelude::*; + +/// The `SecurifiedEntityControl`, address and possibly third party deposit state of some +/// Securified entity. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct SecurifiedPersona { + name: String, + /// The address which is verified to match the `veci` + identity_address: IdentityAddress, + securified_entity_control: SecurifiedEntityControl, +} +impl IsNetworkAware for SecurifiedPersona { + fn network_id(&self) -> NetworkID { + self.address().network_id() + } +} + +impl IsSecurifiedEntity for SecurifiedPersona { + type BaseEntity = Persona; + fn securified_entity_control(&self) -> SecurifiedEntityControl { + self.securified_entity_control() + } + + fn new( + name: impl AsRef, + address: IdentityAddress, + securified_entity_control: SecurifiedEntityControl, + _third_party_deposit: impl Into>, + ) -> Self { + Self { + name: name.as_ref().to_owned(), + identity_address: address, + securified_entity_control, + } + } +} + +impl SecurifiedPersona { + pub fn persona(&self) -> Persona { + Persona::new( + self.name.clone(), + self.address(), + EntitySecurityState::Securified(self.securified_entity_control()), + None, + ) + } + pub fn address(&self) -> IdentityAddress { + self.identity_address.clone() + } + pub fn securified_entity_control(&self) -> SecurifiedEntityControl { + self.securified_entity_control.clone() + } + pub fn third_party_deposit(&self) -> Option { + None + } +} +impl HasSampleValues for SecurifiedPersona { + fn sample() -> Self { + Self::new( + "SecurifiedPersona", + IdentityAddress::sample(), + SecurifiedEntityControl::sample(), + None, + ) + } + fn sample_other() -> Self { + Self::new( + "SecurifiedPersona Other", + IdentityAddress::sample_other(), + SecurifiedEntityControl::sample_other(), + None, + ) + } +} +#[cfg(test)] +mod tests { + + use super::*; + + type Sut = SecurifiedPersona; + + #[test] + fn equality() { + assert_eq!(Sut::sample(), Sut::sample()); + assert_eq!(Sut::sample_other(), Sut::sample_other()); + } + + #[test] + fn inequality() { + assert_ne!(Sut::sample(), Sut::sample_other()); + } +} diff --git a/src/types/sargon_types.rs b/src/types/sargon_types.rs index 32cb0c99..ea75411e 100644 --- a/src/types/sargon_types.rs +++ b/src/types/sargon_types.rs @@ -328,16 +328,14 @@ impl UnhardenedIndex { self.0.to_be_bytes().to_vec() } - pub fn add_n(&self, n: HDPathValue) -> Self { + pub fn add_n(&self, n: HDPathValue) -> Result { let base_index = self.base_index(); - assert!( - (base_index as u64 + n as u64) < BIP32_HARDENED as u64, - "Index would overflow beyond BIP32_HARDENED if we would add {}.", - n - ); + if !(base_index as u64 + n as u64) < BIP32_HARDENED as u64 { + return Err(CommonError::EntityIndexWouldOverflowIfAddedTo); + } - Self::new(self.0 + n) + Ok(Self::new(self.0 + n)) } pub(crate) fn base_index(&self) -> HDPathValue { @@ -349,8 +347,8 @@ impl UnhardenedIndex { #[derive( Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, derive_more::Display, derive_more::Debug, )] -#[display("{}'", self.base_index())] -#[debug("{}'", self.base_index())] +#[display("{}{}", self.base_index(), KeySpace::Unsecurified.indicator())] +#[debug("{}{}", self.base_index(), KeySpace::Unsecurified.indicator())] pub struct UnsecurifiedIndex(HDPathValue); impl UnsecurifiedIndex { /// # Panics @@ -367,15 +365,12 @@ impl UnsecurifiedIndex { self.0.to_be_bytes().to_vec() } - pub fn add_n(&self, n: HDPathValue) -> Self { + pub fn add_n(&self, n: HDPathValue) -> Result { let base_index = self.base_index(); - assert!( - (base_index as u64 + n as u64) < (BIP32_HARDENED + BIP32_SECURIFIED_HALF) as u64, - "Index would overflow beyond BIP32_SECURIFIED_HALF if incremented with {:?}.", - n, - ); - - Self::new(self.0 + n) + if !(base_index as u64 + n as u64) < (BIP32_HARDENED + BIP32_SECURIFIED_HALF) as u64 { + return Err(CommonError::EntityIndexWouldOverflowIfAddedTo); + } + Ok(Self::new(self.0 + n)) } pub(crate) fn base_index(&self) -> HDPathValue { @@ -387,8 +382,8 @@ impl UnsecurifiedIndex { #[derive( Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, derive_more::Display, derive_more::Debug, )] -#[display("{}^", self.base_index())] -#[debug("{}^", self.base_index())] +#[display("{}{}", self.base_index(), KeySpace::Securified.indicator())] +#[debug("{}{}", self.base_index(), KeySpace::Securified.indicator())] pub struct SecurifiedIndex(u32); impl SecurifiedIndex { /// # Panics @@ -406,15 +401,13 @@ impl SecurifiedIndex { self.0.to_be_bytes().to_vec() } - pub fn add_n(&self, n: HDPathValue) -> Self { + pub fn add_n(&self, n: HDPathValue) -> Result { let base_index = self.base_index(); - assert!( - (base_index as u64 + n as u64) < HDPathValue::MAX as u64, - "Index would overflow beyond 2^32 if incremented with {:?}.", - n, - ); + if !(base_index as u64 + n as u64) < HDPathValue::MAX as u64 { + return Err(CommonError::EntityIndexWouldOverflowIfAddedTo); + } - Self::new(self.0 + n) + Ok(Self::new(self.0 + n)) } pub(crate) fn base_index(&self) -> HDPathValue { @@ -468,10 +461,10 @@ impl HDPathComponentHardened { } } - pub fn add_n(&self, n: HDPathValue) -> Self { + pub fn add_n(&self, n: HDPathValue) -> Result { match self { - Self::Unsecurified(u) => u.add_n(n).into(), - Self::Securified(s) => s.add_n(n).into(), + Self::Unsecurified(u) => u.add_n(n).map(Self::from), + Self::Securified(s) => s.add_n(n).map(Self::from), } } @@ -533,7 +526,7 @@ impl Step for HDPathComponent { } fn forward_checked(start: Self, count: usize) -> Option { - start.add_n_checked(count as u32) + start.add_n(count as u32).ok() } fn backward_checked(_start: Self, _count: usize) -> Option { @@ -601,29 +594,16 @@ impl HDPathComponent { /// # Panics /// Panics if self would overflow within its key space. - pub fn add_n_checked(&self, n: HDPathValue) -> Option { - use std::panic; - panic::catch_unwind(|| self.add_n(n)).ok() - } - - /// # Panics - /// Panics if self would overflow within its key space. - pub fn add_n(&self, n: HDPathValue) -> Self { + pub fn add_n(&self, n: HDPathValue) -> Result { match self { - Self::Hardened(h) => h.add_n(n).into(), - Self::Unhardened(u) => u.add_n(n).into(), + Self::Hardened(h) => h.add_n(n).map(Self::from), + Self::Unhardened(u) => u.add_n(n).map(Self::from), } } /// # Panics /// Panics if self would overflow within its keyspace. - pub fn add_assign_one(&mut self) { - *self = self.add_one() - } - - /// # Panics - /// Panics if self would overflow within its keyspace. - pub fn add_one(&self) -> Self { + pub fn add_one(&self) -> Result { self.add_n(1) } @@ -672,7 +652,7 @@ mod tests_hdpathcomp { #[test] fn add_one_successful() { let t = |value: Sut, expected_base_index: HDPathValue| { - let actual = value.add_one(); + let actual = value.add_one().unwrap(); assert_eq!(actual.base_index(), expected_base_index) }; t(Sut::unsecurified_hardening_base_index(0), 1); @@ -741,7 +721,16 @@ impl CAP26KeyKind { } #[repr(u8)] -#[derive(Clone, Copy, PartialEq, Eq, Hash, derive_more::Display, derive_more::Debug)] +#[derive( + Clone, + Copy, + PartialEq, + Eq, + Hash, + derive_more::Display, + derive_more::Debug, + enum_iterator::Sequence, +)] pub enum NetworkID { #[display("Mainnet")] #[debug("0")] @@ -750,12 +739,40 @@ pub enum NetworkID { #[display("Stokenet")] #[debug("1")] Stokenet, + + #[display("Adapanet")] + #[debug("10")] + Adapanet, + + /// Nebunet (0x0b / 0d11 ) + /// + /// The first Betanet of Babylon + #[display("Nebunet")] + #[debug("11")] + Nebunet, + + /// Kisharnet (0x0c / 0d12) + /// + /// The first release candidate of Babylon (RCnet v1) + #[display("Kisharnet")] + #[debug("12")] + Kisharnet, + + /// Ansharnet (0x0d / 0d13) + /// + /// The second release candidate of Babylon (RCnet v2) + #[display("Ansharnet")] + #[debug("13")] + Ansharnet, } impl NetworkID { fn discriminant(&self) -> u8 { core::intrinsics::discriminant_value(self) } + pub fn all() -> IndexSet { + enum_iterator::all::().collect() + } } #[repr(u8)] @@ -887,7 +904,7 @@ impl HierarchicalDeterministicPublicKey { } #[derive(Clone, PartialEq, Eq, std::hash::Hash, derive_more::Debug)] -#[debug("{}", self.debug_str())] +#[debug("{}", self.derivation_entity_index())] pub struct HierarchicalDeterministicFactorInstance { pub factor_source_id: FactorSourceIDFromHash, pub public_key: HierarchicalDeterministicPublicKey, @@ -1032,7 +1049,7 @@ impl HasSampleValues for Hash { pub struct SecurifiedEntityControl { pub matrix: MatrixOfFactorInstances, /// Virtual Entity Creation (Factor)Instance - pub veci: Option, + pub veci: Option, pub access_controller: AccessController, } impl SecurifiedEntityControl { @@ -1042,10 +1059,18 @@ impl SecurifiedEntityControl { pub fn primary_role_instances(&self) -> FactorInstances { self.matrix.primary_role_instances() } + pub fn veci(&self) -> Option { + self.veci.clone() + } + + /// This is wrong... should be Role... + pub fn primary_role(&self) -> MatrixOfFactorInstances { + self.matrix.clone() + } pub fn new( matrix: MatrixOfFactorInstances, access_controller: AccessController, - veci: impl Into>, + veci: impl Into>, ) -> Self { Self { matrix, @@ -1060,14 +1085,14 @@ impl HasSampleValues for SecurifiedEntityControl { Self::new( MatrixOfFactorInstances::sample(), AccessController::sample(), - HierarchicalDeterministicFactorInstance::sample(), + VirtualEntityCreatingInstance::sample(), ) } fn sample_other() -> Self { Self::new( MatrixOfFactorInstances::sample_other(), AccessController::sample_other(), - HierarchicalDeterministicFactorInstance::sample_other(), + VirtualEntityCreatingInstance::sample_other(), ) } } @@ -1343,7 +1368,13 @@ pub trait IsEntityAddress: Sized { } } -pub trait IsEntity: Into + TryFrom + Clone { +pub trait IsNetworkAware { + fn network_id(&self) -> NetworkID; +} + +pub trait IsEntity: + Into + TryFrom + Clone + IsNetworkAware +{ type Address: IsEntityAddress + HasSampleValues + Clone @@ -1361,14 +1392,23 @@ pub trait IsEntity: Into + TryFrom + Clone { third_party_deposit: impl Into>, ) -> Self; - fn unsecurified_mainnet( + fn as_unsecurified(&self) -> Result { + match self.security_state() { + EntitySecurityState::Unsecured(fi) => Ok(UnsecurifiedEntity::new( + self.address(), + fi, + ThirdPartyDepositPreference::default(), // TODO 3rd party + )), + EntitySecurityState::Securified(_) => Err(CommonError::EntityConversionError), + } + } + + fn unsecurified_on_network( name: impl AsRef, + network_id: NetworkID, genesis_factor_instance: HierarchicalDeterministicFactorInstance, ) -> Self { - let address = Self::Address::new( - NetworkID::Mainnet, - genesis_factor_instance.public_key_hash(), - ); + let address = Self::Address::new(network_id, genesis_factor_instance.public_key_hash()); Self::new( name, address, @@ -1377,7 +1417,14 @@ pub trait IsEntity: Into + TryFrom + Clone { ) } - fn securified_mainnet( + fn unsecurified_mainnet( + name: impl AsRef, + genesis_factor_instance: HierarchicalDeterministicFactorInstance, + ) -> Self { + Self::unsecurified_on_network(name, NetworkID::Mainnet, genesis_factor_instance) + } + + fn securified( name: impl AsRef, address: Self::Address, make_matrix: impl Fn() -> MatrixOfFactorInstances, @@ -1400,21 +1447,21 @@ pub trait IsEntity: Into + TryFrom + Clone { ) } - fn network_id(&self) -> NetworkID { - match self.security_state() { - EntitySecurityState::Securified(sec) => { - sec.matrix - .all_factors() - .iter() - .last() - .unwrap() - .public_key - .derivation_path - .network_id - } - EntitySecurityState::Unsecured(fi) => fi.public_key.derivation_path.network_id, - } + fn securified_mainnet( + name: impl AsRef, + address: Self::Address, + make_matrix: impl Fn() -> MatrixOfFactorInstances, + ) -> Self { + assert_eq!(address.network_id(), NetworkID::Mainnet); + let matrix = make_matrix(); + assert!(matrix + .clone() + .all_factors() + .into_iter() + .all(|f| f.derivation_path().network_id == NetworkID::Mainnet)); + Self::securified(name, address, || matrix.clone()) } + fn all_factor_instances(&self) -> HashSet { self.security_state() .all_factor_instances() @@ -1458,30 +1505,29 @@ pub struct AbstractEntity + EntityKin pub third_party_deposit: ThirdPartyDepositPreference, } pub type Account = AbstractEntity; +impl IsNetworkAware for Account { + fn network_id(&self) -> NetworkID { + self.address.network_id() + } +} +impl IsNetworkAware for Persona { + fn network_id(&self) -> NetworkID { + self.address.network_id() + } +} + +impl Persona { + pub fn as_securified(&self) -> Result { + SecurifiedPersona::try_from(self.clone()) + } +} impl Account { pub fn set_name(&mut self, name: impl AsRef) { self.name = name.as_ref().to_owned(); } - pub fn as_securified(&self) -> Result { - match self.security_state() { - EntitySecurityState::Securified(sec) => Ok(SecurifiedEntity::new( - self.address(), - sec, - ThirdPartyDepositPreference::default(), // TODO 3rd party - )), - EntitySecurityState::Unsecured(_) => Err(CommonError::EntityConversionError), - } - } - pub fn as_unsecurified(&self) -> Result { - match self.security_state() { - EntitySecurityState::Unsecured(fi) => Ok(UnsecurifiedEntity::new( - self.address(), - fi, - ThirdPartyDepositPreference::default(), // TODO 3rd party - )), - EntitySecurityState::Securified(_) => Err(CommonError::EntityConversionError), - } + pub fn as_securified(&self) -> Result { + SecurifiedAccount::try_from(self.clone()) } } @@ -1984,6 +2030,11 @@ impl Default for ProfileNetwork { } impl ProfileNetwork { + pub fn contains_entity_by_address(&self, entity_address: &E::Address) -> bool { + self.get_entities_erased(E::kind()) + .into_iter() + .any(|e| e.address() == entity_address.clone().into()) + } pub fn contains_account(&self, address: impl Into) -> bool { let address = address.into(); self.accounts.contains_key(&address) @@ -2043,7 +2094,8 @@ impl ProfileNetwork { let count = self.accounts.len(); let expected_after_insertion = count + accounts.len(); for a in accounts.into_iter() { - self.accounts.insert(a.entity_address(), a); + let already_existed = self.accounts.insert(a.entity_address(), a); + assert!(already_existed.is_none()); } assert_eq!(self.accounts.len(), expected_after_insertion); Ok(()) @@ -2106,6 +2158,12 @@ impl From for AccountAddress { } } impl Profile { + pub fn contains_entity_by_address(&self, entity_address: &E::Address) -> bool { + self.networks + .values() + .any(|n: &ProfileNetwork| n.contains_entity_by_address::(entity_address)) + } + /// # Panics if no BDFS was found pub fn bdfs(&self) -> HDFactorSource { self.factor_sources @@ -2138,27 +2196,41 @@ impl Profile { .unwrap_or_default() } - pub fn get_securified_entities_of_kind_on_network( + pub fn get_securified_entities_of_kind_on_network( &self, - entity_kind: CAP26EntityKind, network_id: NetworkID, - ) -> IndexSet { + ) -> IndexSet { self.get_entities_of_kind_on_network_in_key_space( - entity_kind, + E::kind(), network_id, KeySpace::Securified, ) .into_iter() - .map(|e: AccountOrPersona| { - let control = match e.security_state() { - EntitySecurityState::Securified(control) => control, - _ => unreachable!(), - }; - SecurifiedEntity::new(e.address(), control, e.third_party_deposit_settings()) - }) + .flat_map(|x| E::try_from(x).ok()) .collect() } + // pub fn get_securified_entities_of_kind_on_network( + // &self, + // entity_kind: CAP26EntityKind, + // network_id: NetworkID, + // ) -> IndexSet { + // self.get_entities_of_kind_on_network_in_key_space( + // entity_kind, + // network_id, + // KeySpace::Securified, + // ) + // .into_iter() + // .map(|e: AccountOrPersona| { + // let control = match e.security_state() { + // EntitySecurityState::Securified(control) => control, + // _ => unreachable!(), + // }; + // SecurifiedEntity::new(e.address(), control, e.third_party_deposit_settings()) + // }) + // .collect() + // } + pub fn get_unsecurified_entities_of_kind_on_network( &self, entity_kind: CAP26EntityKind, @@ -2194,8 +2266,8 @@ impl Profile { pub fn securified_accounts_on_network( &self, network_id: NetworkID, - ) -> IndexSet { - self.get_securified_entities_of_kind_on_network(CAP26EntityKind::Account, network_id) + ) -> IndexSet { + self.get_securified_entities_of_kind_on_network(network_id) } pub fn unsecurified_identities_on_network( @@ -2208,8 +2280,8 @@ impl Profile { pub fn securified_identities_on_network( &self, network_id: NetworkID, - ) -> IndexSet { - self.get_securified_entities_of_kind_on_network(CAP26EntityKind::Identity, network_id) + ) -> IndexSet { + self.get_securified_entities_of_kind_on_network(network_id) } pub fn accounts_on_network(&self, network_id: NetworkID) -> IndexSet { @@ -2236,6 +2308,10 @@ impl Profile { self.get_entities() } + pub fn get_personas(&self) -> IndexSet { + self.get_entities() + } + /// assumes mainnet accounts and mainnet personas pub fn new<'a, 'p>( factor_sources: impl IntoIterator, @@ -2292,6 +2368,16 @@ impl Profile { } } + pub fn get_entity( + &self, + address: &E::Address, + ) -> Result { + self.get_entities::() + .into_iter() + .find(|e| e.entity_address() == *address) + .ok_or(CommonError::UnknownEntity) + } + pub fn get_account(&self, address: &AccountAddress) -> Result { self.get_accounts() .into_iter() @@ -2555,6 +2641,24 @@ pub enum CommonError { #[error("Invalid u30")] Invalid30 { bad_value: u32 }, + + #[error("Account not securified")] + AccountNotSecurified, + + #[error("Persona not securified")] + PersonaNotSecurified, + + #[error("Unsupported Non Preset DerivationPath")] + NonStandardDerivationPath, + + #[error("Entity Index would overflow if added to")] + EntityIndexWouldOverflowIfAddedTo, + + #[error("Cache Already Contains FactorInstance with path: {derivation_path}")] + CacheAlreadyContainsFactorInstance { derivation_path: DerivationPath }, + + #[error("Too Few FactorInstances derived")] + FactorInstancesProviderDidNotDeriveEnoughFactors, } #[derive(Clone, Debug, PartialEq, Eq, Hash)]