diff --git a/.vscode/settings.json b/.vscode/settings.json index 6d574aa3..d3d2a1d5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,18 +1,23 @@ { - "cSpell.words": [ - "Caprese", - "FACTORINSTANCE", - "FACTORSOURCE", - "interactor", - "Interactors", - "Keyrings", - "preprocess", - "Quartier", - "Rémy", - "substate", - "txid", - "txids", - "unsecurified", - "Yubikey" - ] + "cSpell.words": [ + "Banksy", + "Caprese", + "FACTORINSTANCE", + "FACTORSOURCE", + "interactor", + "Interactors", + "Keyrings", + "preprocess", + "Quartier", + "Rémy", + "scrypto", + "securify", + "securifying", + "substate", + "txid", + "txids", + "unrecovered", + "unsecurified", + "Yubikey" + ] } diff --git a/Cargo.lock b/Cargo.lock index abd56c43..296862c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -105,12 +105,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "bytes" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" - [[package]] name = "cc" version = "1.0.100" @@ -194,6 +188,18 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" +[[package]] +name = "enum-as-inner" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ffccbb6966c05b32ef8fbac435df276c4ae4d3dc55a8cd0eb9745e6c12f546a" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "env_logger" version = "0.7.1" @@ -235,12 +241,95 @@ dependencies = [ "serde", ] +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + [[package]] name = "futures-core" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -447,6 +536,12 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -634,25 +729,21 @@ dependencies = [ ] [[package]] -name = "sha256" -version = "1.5.0" +name = "signal-hook-registry" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18278f6a914fa3070aa316493f7d2ddfb9ac86ebc06fa3b83bffda487e9065b0" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ - "async-trait", - "bytes", - "hex", - "sha2", - "tokio", + "libc", ] [[package]] -name = "signal-hook-registry" -version = "1.4.2" +name = "slab" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ - "libc", + "autocfg", ] [[package]] @@ -738,7 +829,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" dependencies = [ "backtrace", - "bytes", "libc", "mio", "parking_lot", @@ -774,6 +864,9 @@ dependencies = [ "async-trait", "derive-getters", "derive_more", + "enum-as-inner", + "futures", + "hex", "indexmap", "indexset", "itertools", @@ -783,7 +876,7 @@ dependencies = [ "pretty_env_logger 0.5.0", "rand", "sensible-env-logger", - "sha256", + "sha2", "strum", "thiserror", "uuid", diff --git a/Cargo.toml b/Cargo.toml index 2284c43c..b19fd8cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ indexset = "0.4.0" itertools = "0.13.0" once_cell = "1.19.0" rand = "0.8.5" -sha256 = "1.5.0" +sha2 = "0.10.8" # strum = "0.26.1" strum = { git = "https://github.com/Peternator7/strum/", rev = "f746c3699acf150112e26c00e6c8ca666d8d068d", features = [ "derive", @@ -34,3 +34,6 @@ log = "0.4.20" sensible-env-logger = "0.3.2" pretty_env_logger = { git = "https://github.com/seanmonstar/pretty-env-logger/", rev = "0e238400e18649415dc710c025e99c009a1bb744" } +hex = "0.4.3" +enum-as-inner = "0.6.0" +futures = "0.3.30" diff --git a/src/derivation/tests/derivation_tests.rs b/src/derivation/tests/derivation_tests.rs index 89282da7..cecca1a3 100644 --- a/src/derivation/tests/derivation_tests.rs +++ b/src/derivation/tests/derivation_tests.rs @@ -12,15 +12,15 @@ mod key_derivation_tests { async fn failure_unknown_factor() { let res = KeysCollector::new( IndexSet::new(), - IndexMap::from_iter([( + IndexMap::just(( FactorSourceIDFromHash::fs0(), - IndexSet::from_iter([DerivationPath::new( + IndexSet::just(DerivationPath::new( Mainnet, Account, T9n, HDPathComponent::securified(0), - )]), - )]), + )), + )), Arc::new(TestDerivationInteractors::default()), ); assert!(matches!(res, Err(CommonError::UnknownFactorSource))); @@ -31,7 +31,7 @@ mod key_derivation_tests { let factor_source = fs_at(0); let paths = [0, 1, 2] .into_iter() - .map(|i| DerivationPath::at(Mainnet, Account, T9n, i)) + .map(|i| DerivationPath::unsecurified(Mainnet, Account, T9n, i)) .collect::>(); let collector = KeysCollector::new( HDFactorSource::all(), @@ -53,7 +53,7 @@ mod key_derivation_tests { let factor_source = fs_at(0); let paths = [0, 1, 2] .into_iter() - .map(|i| DerivationPath::at(Mainnet, Account, T9n, i)) + .map(|i| DerivationPath::unsecurified(Mainnet, Account, T9n, i)) .collect::>(); let collector = KeysCollector::new_test([(factor_source.factor_source_id(), paths.clone())]); @@ -75,8 +75,8 @@ mod key_derivation_tests { #[actix_rt::test] async fn multi_keys_multi_factor_sources_single_index_per() { - let path = DerivationPath::account_tx(Mainnet, HDPathComponent::non_hardened(0)); - let paths = IndexSet::from_iter([path]); + let path = DerivationPath::account_tx(Mainnet, HDPathComponent::unsecurified(0)); + let paths = IndexSet::just(path); let factor_sources = HDFactorSource::all(); let collector = KeysCollector::new_test( @@ -112,7 +112,7 @@ mod key_derivation_tests { async fn multi_keys_multi_factor_sources_multi_paths() { let paths = [0, 1, 2] .into_iter() - .map(|i| DerivationPath::at(Mainnet, Account, T9n, i)) + .map(|i| DerivationPath::unsecurified(Mainnet, Account, T9n, i)) .collect::>(); let factor_sources = HDFactorSource::all(); @@ -154,49 +154,49 @@ mod key_derivation_tests { paths.extend( [0, 1, 2] .into_iter() - .map(|i| DerivationPath::at(Mainnet, Account, T9n, i)), + .map(|i| DerivationPath::unsecurified(Mainnet, Account, T9n, i)), ); paths.extend( [0, 1, 2] .into_iter() - .map(|i| DerivationPath::at(Stokenet, Account, T9n, i)), + .map(|i| DerivationPath::unsecurified(Stokenet, Account, T9n, i)), ); paths.extend( [0, 1, 2] .into_iter() - .map(|i| DerivationPath::at(Mainnet, Identity, T9n, i)), + .map(|i| DerivationPath::unsecurified(Mainnet, Identity, T9n, i)), ); paths.extend( [0, 1, 2] .into_iter() - .map(|i| DerivationPath::at(Stokenet, Identity, T9n, i)), + .map(|i| DerivationPath::unsecurified(Stokenet, Identity, T9n, i)), ); paths.extend( [0, 1, 2] .into_iter() - .map(|i| DerivationPath::at(Mainnet, Account, Rola, i)), + .map(|i| DerivationPath::unsecurified(Mainnet, Account, Rola, i)), ); paths.extend( [0, 1, 2] .into_iter() - .map(|i| DerivationPath::at(Stokenet, Account, Rola, i)), + .map(|i| DerivationPath::unsecurified(Stokenet, Account, Rola, i)), ); paths.extend( [0, 1, 2] .into_iter() - .map(|i| DerivationPath::at(Mainnet, Identity, Rola, i)), + .map(|i| DerivationPath::unsecurified(Mainnet, Identity, Rola, i)), ); paths.extend( [0, 1, 2] .into_iter() - .map(|i| DerivationPath::at(Stokenet, Identity, Rola, i)), + .map(|i| DerivationPath::unsecurified(Stokenet, Identity, Rola, i)), ); paths.extend( @@ -209,7 +209,7 @@ mod key_derivation_tests { BIP32_SECURIFIED_HALF + 2, ] .into_iter() - .map(|i| DerivationPath::at(Mainnet, Account, T9n, i)), + .map(|i| DerivationPath::unsecurified(Mainnet, Account, T9n, i)), ); paths.extend( @@ -222,7 +222,7 @@ mod key_derivation_tests { BIP32_SECURIFIED_HALF + 2, ] .into_iter() - .map(|i| DerivationPath::at(Stokenet, Account, T9n, i)), + .map(|i| DerivationPath::unsecurified(Stokenet, Account, T9n, i)), ); paths.extend( @@ -235,7 +235,7 @@ mod key_derivation_tests { BIP32_SECURIFIED_HALF + 2, ] .into_iter() - .map(|i| DerivationPath::at(Mainnet, Identity, T9n, i)), + .map(|i| DerivationPath::unsecurified(Mainnet, Identity, T9n, i)), ); paths.extend( @@ -248,7 +248,7 @@ mod key_derivation_tests { BIP32_SECURIFIED_HALF + 2, ] .into_iter() - .map(|i| DerivationPath::at(Stokenet, Identity, T9n, i)), + .map(|i| DerivationPath::unsecurified(Stokenet, Identity, T9n, i)), ); paths.extend( @@ -261,7 +261,7 @@ mod key_derivation_tests { BIP32_SECURIFIED_HALF + 2, ] .into_iter() - .map(|i| DerivationPath::at(Mainnet, Account, Rola, i)), + .map(|i| DerivationPath::unsecurified(Mainnet, Account, Rola, i)), ); paths.extend( @@ -274,7 +274,7 @@ mod key_derivation_tests { BIP32_SECURIFIED_HALF + 2, ] .into_iter() - .map(|i| DerivationPath::at(Stokenet, Account, Rola, i)), + .map(|i| DerivationPath::unsecurified(Stokenet, Account, Rola, i)), ); paths.extend( @@ -287,7 +287,7 @@ mod key_derivation_tests { BIP32_SECURIFIED_HALF + 2, ] .into_iter() - .map(|i| DerivationPath::at(Mainnet, Identity, Rola, i)), + .map(|i| DerivationPath::unsecurified(Mainnet, Identity, Rola, i)), ); paths.extend( @@ -300,7 +300,7 @@ mod key_derivation_tests { BIP32_SECURIFIED_HALF + 2, ] .into_iter() - .map(|i| DerivationPath::at(Stokenet, Identity, Rola, i)), + .map(|i| DerivationPath::unsecurified(Stokenet, Identity, Rola, i)), ); let factor_sources = HDFactorSource::all(); @@ -383,7 +383,7 @@ mod key_derivation_tests { entity_kind, key_kind, Expected { - index: HDPathComponent::non_hardened(BIP32_SECURIFIED_HALF), + index: HDPathComponent::unsecurified(BIP32_SECURIFIED_HALF), }, ) .await @@ -421,7 +421,7 @@ mod key_derivation_tests { entity_kind, key_kind, Expected { - index: HDPathComponent::non_hardened(0), + index: HDPathComponent::unsecurified(0), }, ) .await diff --git a/src/lib.rs b/src/lib.rs index 33aaebac..54b04f10 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,9 +2,13 @@ #![feature(core_intrinsics)] #![feature(iter_repeat_n)] #![feature(async_closure)] +#![allow(unused_imports)] +#![feature(step_trait)] mod derivation; +mod recovery; mod samples; +mod securify; mod signing; mod types; @@ -14,8 +18,12 @@ mod testing; pub mod prelude { pub use crate::derivation::*; - #[allow(unused_imports)] + pub use crate::recovery::*; + pub(crate) use crate::samples::*; + + pub use crate::securify::*; + pub use crate::signing::*; pub use crate::types::*; @@ -23,12 +31,15 @@ pub mod prelude { pub(crate) use crate::testing::*; pub(crate) use derive_getters::Getters; + pub(crate) use enum_as_inner::EnumAsInner; pub(crate) use indexmap::{IndexMap, IndexSet}; pub(crate) use itertools::Itertools; pub(crate) use std::cell::RefCell; pub(crate) use std::time::SystemTime; pub(crate) use uuid::Uuid; + pub(crate) use sha2::{Digest, Sha256}; + pub(crate) use std::{ collections::{HashMap, HashSet}, sync::Arc, diff --git a/src/recovery/mod.rs b/src/recovery/mod.rs new file mode 100644 index 00000000..3780cf36 --- /dev/null +++ b/src/recovery/mod.rs @@ -0,0 +1,3 @@ +mod recover_entity; + +pub use recover_entity::*; diff --git a/src/recovery/recover_entity.rs b/src/recovery/recover_entity.rs new file mode 100644 index 00000000..d1a7a110 --- /dev/null +++ b/src/recovery/recover_entity.rs @@ -0,0 +1,1021 @@ +use std::{hash::Hash, ops::Add, sync::RwLock}; + +use crate::prelude::*; + +impl Profile { + async fn new_entity( + &mut self, + network_id: NetworkID, + name: impl AsRef, + factor_source_id: FactorSourceIDFromHash, + gateway: Arc, + ) -> E { + assert!(self + .factor_sources + .iter() + .map(|f| f.factor_source_id()) + .contains(&factor_source_id)); + + let entity_kind = E::kind(); + let key_kind = CAP26KeyKind::T9n; + let key_space = KeySpace::Unsecurified; + let name = name.as_ref(); + + let index_assigner = NextFreeIndexAssigner::live(); + + let base = + index_assigner.derivation_index_for_factor_source(NextFreeIndexAssignerRequest { + network_id, + factor_source_id, + key_space, + entity_kind, + profile: self, + }); + + let mut genesis_factor_and_address: Option<( + HierarchicalDeterministicFactorInstance, + E::Address, + )> = None; + for index in base..(base.add_n(50)) { + let derivation_path = DerivationPath::new(network_id, entity_kind, key_kind, index); + let factor = HierarchicalDeterministicFactorInstance::new( + HierarchicalDeterministicPublicKey::mocked_with(derivation_path, &factor_source_id), + factor_source_id, + ); + + let public_key_hash = factor.public_key_hash(); + + let is_public_key_hash_known_by_gateway = gateway + .is_key_hash_known(public_key_hash.clone()) + .await + .unwrap(); + + let is_address_formed_by_key_already_in_profile = self + .get_entities::() + .iter() + .any(|e| e.address().public_key_hash() == public_key_hash); + let is_index_taken = + is_public_key_hash_known_by_gateway || is_address_formed_by_key_already_in_profile; + + if is_index_taken { + continue; + } else { + let address = E::Address::new(network_id, public_key_hash); + genesis_factor_and_address = Some((factor, address)); + break; + } + } + + let (genesis_factor, address) = genesis_factor_and_address.unwrap(); + + let entity = E::new( + name, + address, + EntitySecurityState::Unsecured(genesis_factor), + ); + + let erased = Into::::into(entity.clone()); + + match erased { + AccountOrPersona::AccountEntity(account) => { + self.accounts.insert(account.entity_address(), account); + } + AccountOrPersona::PersonaEntity(persona) => { + self.personas.insert(persona.entity_address(), persona); + } + }; + + entity + } + + pub async fn new_account( + &mut self, + network_id: NetworkID, + name: impl AsRef, + factor_source_id: FactorSourceIDFromHash, + gateway: Arc, + ) -> Account { + self.new_entity(network_id, name, factor_source_id, gateway) + .await + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct OnChainEntityUnsecurified { + address: AddressOfAccountOrPersona, + owner_keys: Vec, +} +impl OnChainEntityUnsecurified { + pub fn new( + address: impl Into, + owner_keys: Vec, + ) -> Self { + Self { + address: address.into(), + owner_keys, + } + } + + pub fn owner_keys(&self) -> HashSet { + self.owner_keys.iter().cloned().collect() + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct OnChainEntitySecurified { + address: AddressOfAccountOrPersona, + access_controller: AccessController, + owner_keys: Vec, +} + +impl OnChainEntitySecurified { + pub fn new( + address: impl Into, + access_controller: AccessController, + owner_keys: Vec, + ) -> Self { + Self { + address: address.into(), + access_controller, + owner_keys, + } + } + pub fn owner_keys(&self) -> HashSet { + self.owner_keys.iter().cloned().collect() + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash, EnumAsInner)] +pub enum OnChainEntityState { + Unsecurified(OnChainEntityUnsecurified), + Securified(OnChainEntitySecurified), +} +impl OnChainEntityState { + fn unsecurified(unsecurified: OnChainEntityUnsecurified) -> Self { + Self::Unsecurified(unsecurified) + } + + pub fn unsecurified_with( + address: impl Into, + owner_key: PublicKeyHash, + ) -> Self { + Self::unsecurified(OnChainEntityUnsecurified::new(address, vec![owner_key])) + } + + fn securified(securified: OnChainEntitySecurified) -> Self { + Self::Securified(securified) + } + + pub fn securified_with( + address: impl Into, + access_controller: AccessController, + owner_keys: Vec, + ) -> Self { + Self::securified(OnChainEntitySecurified::new( + address, + access_controller.clone(), + owner_keys, + )) + } +} + +impl OnChainEntityState { + #[allow(unused)] + fn address(&self) -> AddressOfAccountOrPersona { + match self { + OnChainEntityState::Unsecurified(account) => account.address.clone(), + OnChainEntityState::Securified(account) => account.address.clone(), + } + } + + fn owner_keys(&self) -> HashSet { + match self { + OnChainEntityState::Unsecurified(account) => account.owner_keys(), + OnChainEntityState::Securified(account) => account.owner_keys(), + } + } +} + +#[async_trait::async_trait] +pub trait GatewayReadonly: Sync + Send { + async fn is_key_hash_known(&self, hash: PublicKeyHash) -> Result; + + async fn get_entity_addresses_of_by_public_key_hashes( + &self, + hashes: HashSet, + ) -> Result>>; + + async fn get_on_chain_entity( + &self, + address: AddressOfAccountOrPersona, + ) -> Result>; + + async fn get_on_chain_account( + &self, + account_address: &AccountAddress, + ) -> Result> { + self.get_on_chain_entity(account_address.clone().into()) + .await + } + + async fn get_owner_key_hashes( + &self, + address: AddressOfAccountOrPersona, + ) -> Result>> { + let on_chain_account = self.get_on_chain_entity(address).await?; + return Ok(on_chain_account.map(|account| account.owner_keys().clone())); + } + + async fn is_securified(&self, address: AddressOfAccountOrPersona) -> Result { + let entity = self.get_on_chain_entity(address).await?; + Ok(entity.map(|x| x.is_securified()).unwrap_or(false)) + } +} + +#[async_trait::async_trait] +pub trait Gateway: GatewayReadonly { + async fn simulate_network_activity_for(&self, owner: AddressOfAccountOrPersona) -> Result<()>; + + async fn set_securified_entity( + &self, + securified: SecurifiedEntityControl, + owner: AddressOfAccountOrPersona, + ) -> Result<()>; + + async fn set_securified_account( + &self, + securified: SecurifiedEntityControl, + owner: &AccountAddress, + ) -> Result<()> { + self.set_securified_entity(securified, owner.clone().into()) + .await + } + + async fn set_securified_persona( + &self, + securified: SecurifiedEntityControl, + owner: &IdentityAddress, + ) -> Result<()> { + self.set_securified_entity(securified, owner.clone().into()) + .await + } +} + +const RECOVERY_BATCH_SIZE_DERIVATION_ENTITY_INDEX: HDPathValue = 50; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct UncoveredEntity { + pub on_chain: OnChainEntityState, + pub key_hash_to_factor_instances: + HashMap, +} +impl UncoveredEntity { + pub fn new( + on_chain: OnChainEntityState, + key_hash_to_factor_instances: HashMap< + PublicKeyHash, + HierarchicalDeterministicFactorInstance, + >, + ) -> Self { + Self { + on_chain, + key_hash_to_factor_instances, + } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct EntityRecoveryOutcome +where + E: IsEntity + Hash + Eq, +{ + pub recovered_unsecurified: IndexSet, + pub recovered_securified: IndexSet, + pub unrecovered: Vec, // want `IndexSet` but is not `Hash` +} +impl EntityRecoveryOutcome { + pub fn new( + recovered_unsecurified: impl IntoIterator, + recovered_securified: impl IntoIterator, + unrecovered: impl IntoIterator, + ) -> Self { + Self { + recovered_unsecurified: recovered_unsecurified.into_iter().collect(), + recovered_securified: recovered_securified.into_iter().collect(), + unrecovered: unrecovered.into_iter().collect(), + } + } +} + +/// A first implementation of Recovery of Securified entities, working POC using +/// Entity Indexing heuristics - `CEI - strategy 1` - Canonical Entity Indexing, +/// as described in [doc]. +/// +/// N.B. This is a simplified version of the algorithm, which does not allow +/// users to trigger "Scan More" action - for which we will continue to derive +/// more PublicKeys for each factor source at "next batch" of derivation indices. +/// +/// Here follows an executive summary of the algorithm: +/// A. User inputs a list of FactorSources +/// B. Create a set of derivation paths, both for securified and unsecurified entities +/// C. For each factor source we derive PublicKey's at **all paths** +/// D. Create PublicKeyHash'es for each PublicKey +/// E. Ensure to retain which (FactorSource, DerivationPath) tuple was for each PublicKeyHash +/// F. Query gateway for EntityAddress referencing each PublicKeyHash +/// G. Query gateway with each EntityAddress to get: AccessController's ScryptoAccessRule or single `owner_key hash +/// H. "Play a match making game" between locally calculated PublicKeyHash'es and the ones downloaded from Gateway +/// I. For each EntityAddress with single `owner_key` create an Unsecurified Entity +/// J. for each with ScryptoAccessRule try to map the `ScryptoAccessRule` into a `MatrixOfPublicKeyHashes`, then try to map that +/// into a `MatrixOfFactorInstances` by looking up the locally derived factor instances (PublicKeys). +/// K. For each EntityAddress which we failed to match all PublicKeyHashes, ask user if should would like to +/// continue the search, by deriving keys using another batch of derivation paths. +/// L. Return the results, which is three sets: recovered_unsecurified, recovered_securified, unrecovered +/// +/// [doc]: https://radixdlt.atlassian.net/wiki/spaces/AT/pages/3640655873/Yet+Another+Page+about+Derivation+Indices +pub async fn recover_entity( + network_id: NetworkID, + factor_sources: impl IntoIterator, + key_derivation_interactors: Arc, + gateway: Arc, +) -> Result> { + // A. User inputs a list of FactorSources + let entity_kind = E::kind(); + let factor_sources = factor_sources.into_iter().collect::>(); + + // B. Create a set of derivation paths, both for securified and unsecurified entities + let map_paths = { + let index_range = 0..RECOVERY_BATCH_SIZE_DERIVATION_ENTITY_INDEX; + let make_paths = + |make_entity_index: fn(HDPathValue) -> HDPathComponent| -> IndexSet { + index_range + .clone() + .map(make_entity_index) + .map(|i| DerivationPath::new(network_id, entity_kind, CAP26KeyKind::T9n, i)) + .collect::>() + }; + + let paths_unsecurified = make_paths(HDPathComponent::unsecurified); + let paths_securified = make_paths(HDPathComponent::securified); + let mut all_paths = IndexSet::::new(); + all_paths.extend(paths_unsecurified); + all_paths.extend(paths_securified); + + let mut map_paths = IndexMap::>::new(); + for factor_source in factor_sources.iter() { + map_paths.insert(factor_source.factor_source_id(), all_paths.clone()); + } + map_paths + }; + + let (addresses_per_hash, map_hash_to_factor) = { + // C. For each factor source we derive PublicKey's at **all paths** + let keys_collector = + KeysCollector::new(factor_sources, map_paths, key_derivation_interactors).unwrap(); + + let factor_instances = keys_collector.collect_keys().await.all_factors(); + + // D. Create PublicKeyHash'es for each PublicKey + let map_hash_to_factor = factor_instances + .into_iter() + .map(|f| (f.public_key_hash(), f.clone())) + .collect::>(); + + // E. Ensure to retain which (FactorSource, DerivationPath) tuple was for each PublicKeyHash + // F. Query gateway for EntityAddress referencing each PublicKeyHash + let untyped_addresses_per_hash = gateway + .get_entity_addresses_of_by_public_key_hashes( + map_hash_to_factor.keys().cloned().collect::>(), + ) + .await?; + + let addresses_per_hash = untyped_addresses_per_hash + .into_iter() + .map(|(k, v)| { + let typed_address = v + .into_iter() + .map(|a| E::Address::try_from(a).map_err(|_| CommonError::Failure)) + .collect::>>()?; + + Ok((k, typed_address)) + }) + .collect::>>>()?; + + (addresses_per_hash, map_hash_to_factor) + }; + + // G. Query gateway with each EntityAddress to get: AccessController's ScryptoAccessRule or single `owner_key hash + let (address_to_factor_instances_map, unsecurified_addresses, securified_addresses) = { + let mut unsecurified_addresses = HashSet::::new(); + let mut securified_addresses = HashSet::::new(); + + let mut address_to_factor_instances_map = + HashMap::>::new(); + + for (hash, addresses) in addresses_per_hash.iter() { + if addresses.is_empty() { + unreachable!("We should never create empty sets"); + } + if addresses.len() > 1 { + panic!("Violation of Axiom 1: same key is used in many entities") + } + let address = addresses.iter().last().unwrap(); + + let factor_instance = map_hash_to_factor.get(hash).unwrap(); + if let Some(existing) = address_to_factor_instances_map.get_mut(address) { + existing.insert(factor_instance.clone()); + } else { + address_to_factor_instances_map + .insert(address.clone(), HashSet::just(factor_instance.clone())); + } + + let is_securified = gateway.is_securified(address.clone().into()).await?; + + if is_securified { + securified_addresses.insert(address.clone()); + } else { + unsecurified_addresses.insert(address.clone()); + } + } + + ( + address_to_factor_instances_map, + unsecurified_addresses, + securified_addresses, + ) + }; + + // H. "Play a match making game" between locally calculated PublicKeyHash'es and the ones downloaded from Gateway + + // I. For each EntityAddress with single `owner_key` create an Unsecurified Entity + let unsecurified_entities = unsecurified_addresses + .into_iter() + .map(|a| { + let factor_instances = address_to_factor_instances_map.get(&a).unwrap(); + assert_eq!( + factor_instances.len(), + 1, + "Expected single factor since unsecurified" + ); + let factor_instance = factor_instances.iter().last().unwrap(); + let security_state = EntitySecurityState::Unsecured(factor_instance.clone()); + E::new( + format!("Recovered Unsecurified: {:?}", a), + a, + security_state, + ) + }) + .collect::>(); + + let mut securified_entities = HashSet::::new(); + let mut unrecovered_entities = Vec::::new(); + + // J. for each with ScryptoAccessRule try to map the `ScryptoAccessRule` into a `MatrixOfPublicKeyHashes`, then try to map that + // into a `MatrixOfFactorInstances` by looking up the locally derived factor instances (PublicKeys). + for a in securified_addresses { + let on_chain_entity = gateway + .get_on_chain_entity(a.clone().into()) + .await + .unwrap() + .unwrap() + .as_securified() + .unwrap() + .clone(); + + // K. [NOT IMPLEMENTED YET] For each EntityAddress which we failed to match all PublicKeyHashes, ask user if should would like to + // continue the search, by deriving keys using another batch of derivation paths. + + let mut fail = || { + let unrecovered_entity = UncoveredEntity::new( + OnChainEntityState::Securified(on_chain_entity.clone()), + HashMap::new(), // TODO: fill this + ); + warn!("Could not recover entity: {:?}", unrecovered_entity); + unrecovered_entities.push(unrecovered_entity); + }; + + let Ok(matrix_of_hashes) = MatrixOfKeyHashes::try_from( + on_chain_entity + .clone() + .access_controller + .metadata + .scrypto_access_rules, + ) else { + fail(); + continue; + }; + + let mut threshold_factor_instances = IndexSet::new(); + let mut override_factor_instances = IndexSet::new(); + + for threshold_factor_hash in matrix_of_hashes.threshold_factors.iter() { + let Some(factor_instance) = map_hash_to_factor.get(threshold_factor_hash) else { + warn!( + "Missing THRESHOLD factor instance for hash: {:?}", + threshold_factor_hash + ); + continue; + }; + threshold_factor_instances.insert(factor_instance.clone()); + } + + for override_factor_hash in matrix_of_hashes.override_factors.iter() { + let Some(factor_instance) = map_hash_to_factor.get(override_factor_hash) else { + warn!( + "Missing OVERRIDE factor instance for hash: {:?}", + override_factor_hash + ); + continue; + }; + override_factor_instances.insert(factor_instance.clone()); + } + + if threshold_factor_instances.len() < matrix_of_hashes.threshold as usize { + warn!("Not enough threshold factors"); + fail(); + continue; + } + + let sec = SecurifiedEntityControl::new( + MatrixOfFactorInstances::new( + threshold_factor_instances, + matrix_of_hashes.threshold, + override_factor_instances, + ), + on_chain_entity.access_controller, + ); + let security_state = EntitySecurityState::Securified(sec); + let recovered_securified_entity = + E::new(format!("Recovered Securified: {:?}", a), a, security_state); + assert!(securified_entities.insert(recovered_securified_entity)); + } + + // L. Return the results, which is three sets: recovered_unsecurified, recovered_securified, unrecovered + Ok(EntityRecoveryOutcome::::new( + unsecurified_entities, + securified_entities, + Vec::new(), + )) +} + +pub async fn recover_accounts( + network_id: NetworkID, + factor_sources: impl IntoIterator, + key_derivation_interactors: Arc, + gateway: Arc, +) -> Result> { + recover_entity( + network_id, + factor_sources, + key_derivation_interactors, + gateway, + ) + .await +} + +pub async fn recover_personas( + network_id: NetworkID, + factor_sources: impl IntoIterator, + key_derivation_interactors: Arc, + gateway: Arc, +) -> Result> { + recover_entity( + network_id, + factor_sources, + key_derivation_interactors, + gateway, + ) + .await +} + +#[cfg(test)] +pub struct TestGateway { + /// contains only current state for each entity + entities: RwLock>, + + /// contains historic state, we only ever add to this set, never remove. + known_hashes: RwLock>, +} +#[cfg(test)] +impl Default for TestGateway { + fn default() -> Self { + Self { + known_hashes: RwLock::new(HashSet::new()), + entities: RwLock::new(HashMap::new()), + } + } +} + +#[cfg(test)] +impl TestGateway { + #[allow(unused)] + pub fn debug_print(&self) { + println!( + "⛩️ known_hashes: {:?}", + self.known_hashes.try_read().unwrap() + ); + println!("⛩️ entities: {:?}", self.entities.try_read().unwrap().keys()); + } +} + +#[cfg(test)] +#[async_trait::async_trait] +impl GatewayReadonly for TestGateway { + async fn is_key_hash_known(&self, hash: PublicKeyHash) -> Result { + let is_known = self.known_hashes.try_read().unwrap().contains(&hash); + Ok(is_known) + } + async fn get_entity_addresses_of_by_public_key_hashes( + &self, + hashes: HashSet, + ) -> Result>> { + let entities = self.entities.try_read().unwrap(); + let states = entities.values(); + + Ok(hashes + .iter() + .filter_map(|k| { + // N.B. we want this to always be single element (Axiom 1). + let mut entities_references_hash = HashSet::::new(); + for state in states.clone().filter(|x| x.owner_keys().contains(k)) { + entities_references_hash.insert(state.address()); + } + if entities_references_hash.is_empty() { + None + } else { + Some((k.clone(), entities_references_hash)) + } + }) + .collect::>>()) + } + + async fn get_on_chain_entity( + &self, + address: AddressOfAccountOrPersona, + ) -> Result> { + Ok(self.entities.try_read().unwrap().get(&address).cloned()) + } +} +#[cfg(test)] +impl TestGateway { + async fn assert_not_securified(&self, address: &AddressOfAccountOrPersona) -> Result<()> { + let is_already_securified = self.is_securified(address.clone()).await?; + assert!( + !is_already_securified, + "Cannot unsecurify an already securified entity" + ); + Ok(()) + } + + fn contains(&self, address: &AddressOfAccountOrPersona) -> bool { + self.entities.try_read().unwrap().contains_key(address) + } +} + +#[cfg(test)] +#[async_trait::async_trait] +impl Gateway for TestGateway { + async fn simulate_network_activity_for(&self, owner: AddressOfAccountOrPersona) -> Result<()> { + self.assert_not_securified(&owner).await?; + + let owner_key = owner.public_key_hash(); + + if self.contains(&owner) || self.known_hashes.try_read().unwrap().contains(&owner_key) { + panic!("update not supported") + } else { + self.entities.try_write().unwrap().insert( + owner.clone(), + OnChainEntityState::unsecurified_with(owner, owner_key.clone()), + ); + self.known_hashes.try_write().unwrap().insert(owner_key); + } + Ok(()) + } + + async fn set_securified_entity( + &self, + securified: SecurifiedEntityControl, + owner: AddressOfAccountOrPersona, + ) -> Result<()> { + self.assert_not_securified(&owner).await?; + + let owner_keys = securified + .matrix + .all_factors() + .iter() + .map(|f| f.public_key_hash()) + .collect_vec(); + + if self.contains(&owner) { + self.entities.try_write().unwrap().remove(&owner); + } + + self.known_hashes + .try_write() + .unwrap() + .extend(owner_keys.clone()); + + self.entities.try_write().unwrap().insert( + owner.clone(), + OnChainEntityState::securified_with( + owner, + securified.access_controller.clone(), + owner_keys, + ), + ); + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use std::{ + borrow::BorrowMut, + future::{Future, IntoFuture}, + }; + + use futures::future::BoxFuture; + + use super::*; + + #[test] + fn public_key_hash_is_unique() { + let f = &FactorSourceIDFromHash::fs0(); + type PK = HierarchicalDeterministicPublicKey; + let n: usize = 10; + let pub_keys = HashSet::::from_iter( + (0..n as HDPathValue) + .map(|i| { + DerivationPath::account_tx(NetworkID::Mainnet, HDPathComponent::unsecurified(i)) + }) + .map(|p| PK::mocked_with(p, f)), + ); + assert_eq!(pub_keys.len(), n); + let hashes = pub_keys.iter().map(|k| k.hash()).collect::>(); + + assert_eq!(hashes.len(), n); + } + + async fn do_test< + E: IsEntity + Hash + Eq + Sync, + Fut: Future>, + F: FnOnce(Arc) -> Fut, + >( + network_id: NetworkID, + all_factors: IndexSet, + setup: F, + assert: impl FnOnce(IndexSet, EntityRecoveryOutcome) + 'static, + ) { + let gateway = Arc::new(TestGateway::default()); + + let interactors = Arc::new(TestDerivationInteractors::default()); + + let entities = setup(gateway.clone()).await; + + let recovered = recover_entity::(network_id, all_factors, interactors, gateway) + .await + .unwrap(); + + assert(entities, recovered); + } + + #[actix_rt::test] + async fn recovery_of_single_many_securified_accounts() { + let all_factors = HDFactorSource::all(); + + do_test( + NetworkID::Mainnet, + all_factors, + |gateway| { + Box::pin(async move { + let securified_accounts = IndexSet::::from_iter([ + Account::a2(), + Account::a3(), + Account::a4(), + Account::a5(), + Account::a6(), + Account::a7(), + ]); + + for account in securified_accounts.iter() { + gateway + .set_securified_account( + account.security_state.as_securified().unwrap().clone(), + &account.entity_address(), + ) + .await + .unwrap(); + } + + securified_accounts + }) + }, + |known, recovered| { + let recovered_unsecurified_accounts = recovered.recovered_unsecurified; + assert_eq!(recovered_unsecurified_accounts.len(), 0); + + let recovered_securified_accounts = recovered.recovered_securified; + assert_eq!(recovered_securified_accounts.len(), known.len()); + + assert_eq!( + recovered_securified_accounts + .iter() + .map(|a| a.security_state()) + .collect::>(), + known + .iter() + .map(|a| a.security_state()) + .collect::>(), + ); + }, + ) + .await; + } + + #[actix_rt::test] + async fn recovery_of_unsecurified_accounts_only() { + let all_factors = HDFactorSource::all(); + + do_test( + NetworkID::Mainnet, + all_factors, + |gateway| { + Box::pin(async move { + let securified_accounts = + IndexSet::::from_iter([Account::a0(), Account::a1()]); + + for account in securified_accounts.iter() { + gateway + .simulate_network_activity_for(account.address()) + .await + .unwrap(); + } + + securified_accounts + }) + }, + |known, recovered| { + assert_eq!(recovered.recovered_securified.len(), 0); + + let recovered_unsecurified_accounts = recovered.recovered_unsecurified; + assert_eq!(recovered_unsecurified_accounts.len(), known.len()); + + assert_eq!( + recovered_unsecurified_accounts + .iter() + .map(|a| a.security_state()) + .collect::>(), + known + .iter() + .map(|a| a.security_state()) + .collect::>(), + ); + }, + ) + .await; + } + + #[actix_rt::test] + async fn recovery_of_single_many_securified_personas() { + let all_factors = HDFactorSource::all(); + + do_test( + NetworkID::Mainnet, + all_factors.clone(), + |gateway| { + Box::pin(async move { + let securified_personas = IndexSet::::from_iter([ + Persona::p2(), + Persona::p3(), + Persona::p4(), + Persona::p5(), + Persona::p6(), + Persona::p7(), + ]); + + for persona in securified_personas.iter() { + gateway + .set_securified_persona( + persona.security_state.as_securified().unwrap().clone(), + &persona.entity_address(), + ) + .await + .unwrap(); + } + securified_personas + }) + }, + |known: IndexSet, recovered| { + assert_eq!(recovered.recovered_unsecurified.len(), 0); + + let recovered_securified_personas = recovered.recovered_securified; + assert_eq!(recovered_securified_personas.len(), known.len()); + + assert_eq!( + recovered_securified_personas + .iter() + .map(|a| a.security_state()) + .collect::>(), + known + .iter() + .map(|a| a.security_state()) + .collect::>(), + ); + }, + ) + .await; + } + + #[actix_rt::test] + async fn recovery_of_accounts_mixed_securified_and_non() { + let all_factors = HDFactorSource::all(); + + do_test( + NetworkID::Mainnet, + all_factors.clone(), + |gateway| { + Box::pin(async move { + let mut profile = Profile::new(all_factors.clone(), [], []); + + let alice_address = profile + .new_account(NetworkID::Mainnet, "alice", fs_id_at(0), gateway.clone()) + .await + .entity_address(); + + let interactors = Arc::new(TestDerivationInteractors::default()); + + securify( + alice_address.clone(), + MatrixOfFactorSources::new( + [fs_at(0), fs_at(1), fs_at(2), fs_at(3)], + 3, + [fs_at(6)], + ), + &mut profile, + interactors.clone(), + gateway.clone(), + ) + .await + .unwrap(); + + let bob_address = profile + .new_account(NetworkID::Mainnet, "bob", fs_id_at(1), gateway.clone()) + .await + .entity_address(); + + securify( + bob_address.clone(), + MatrixOfFactorSources::new([fs_at(1), fs_at(3)], 2, [fs_at(7)]), + &mut profile, + interactors, + gateway.clone(), + ) + .await + .unwrap(); + + let charlie_address = profile + .new_account(NetworkID::Mainnet, "charlie", fs_id_at(1), gateway.clone()) + .await + .entity_address(); + + let accounts: IndexSet = profile.get_accounts(); + + assert_eq!(accounts.len(), 3); + + let alice = profile.account_by_address(alice_address).unwrap(); + let bob = profile.account_by_address(bob_address).unwrap(); + let charlie = profile.account_by_address(charlie_address).unwrap(); + + gateway + .simulate_network_activity_for(charlie.address()) + .await + .unwrap(); + + assert!(alice.is_securified()); + assert!(bob.is_securified()); + assert!(!charlie.is_securified()); + + assert_eq!( + charlie + .security_state + .into_unsecured() + .unwrap() + .derivation_path() + .index + .index(), + 1 // second time that factor source was used. + ); + accounts + }) + }, + move |known: IndexSet, recovered| { + assert!(recovered.unrecovered.is_empty()); + assert_eq!( + known.len(), + recovered.recovered_securified.len() + recovered.recovered_unsecurified.len() + ); + }, + ) + .await; + } +} diff --git a/src/samples/sample_values.rs b/src/samples/sample_values.rs index 74afd4f1..d4ce35d0 100644 --- a/src/samples/sample_values.rs +++ b/src/samples/sample_values.rs @@ -232,20 +232,78 @@ impl MatrixOfFactorInstances { } } +impl HierarchicalDeterministicFactorInstance { + /// 0 | unsecurified | device + pub fn fi0(entity_kind: CAP26EntityKind) -> Self { + Self::mainnet_tx( + entity_kind, + HDPathComponent::unsecurified(0), + FactorSourceIDFromHash::fs0(), + ) + } + + /// Account: 0 | unsecurified | device + pub fn fia0() -> Self { + Self::fi0(CAP26EntityKind::Account) + } + /// Identity: 0 | unsecurified | device + pub fn fii0() -> Self { + Self::fi0(CAP26EntityKind::Identity) + } + + /// 1 | unsecurified | ledger + pub fn fi1(entity_kind: CAP26EntityKind) -> Self { + Self::mainnet_tx( + entity_kind, + HDPathComponent::unsecurified(1), + FactorSourceIDFromHash::fs1(), + ) + } + + /// Account: 1 | unsecurified | ledger + pub fn fia1() -> Self { + Self::fi1(CAP26EntityKind::Account) + } + /// Identity: 1 | unsecurified | ledger + pub fn fii1() -> Self { + Self::fi1(CAP26EntityKind::Identity) + } + + /// 8 | Unsecurified { Device } (fs10) + pub fn fi10(entity_kind: CAP26EntityKind) -> Self { + Self::mainnet_tx( + entity_kind, + HDPathComponent::unsecurified(8), + FactorSourceIDFromHash::fs10(), + ) + } + + /// Account: 8 | Unsecurified { Device } (fs10) + pub fn fia10() -> Self { + Self::fi10(CAP26EntityKind::Account) + } + + /// Identity: 8 | Unsecurified { Device } (fs10) + pub fn fii10() -> Self { + Self::fi10(CAP26EntityKind::Identity) + } +} + impl Account { /// Alice | 0 | Unsecurified { Device } pub(crate) fn a0() -> Self { - Self::unsecurified_mainnet(0, "Alice", FactorSourceIDFromHash::fs0()) + Self::unsecurified_mainnet("Alice", HierarchicalDeterministicFactorInstance::fia0()) } /// Bob | 1 | Unsecurified { Ledger } pub(crate) fn a1() -> Self { - Self::unsecurified_mainnet(1, "Bob", FactorSourceIDFromHash::fs1()) + Self::unsecurified_mainnet("Bob", HierarchicalDeterministicFactorInstance::fia1()) } /// Carla | 2 | Securified { Single Threshold only } pub(crate) fn a2() -> Self { - Self::securified_mainnet(2, "Carla", |idx| { + Self::securified_mainnet("Carla", AccountAddress::sample_2(), || { + let idx = HDPathComponent::securified(2); MatrixOfFactorInstances::m2(HierarchicalDeterministicFactorInstance::f( Self::entity_kind(), idx, @@ -255,7 +313,8 @@ impl Account { /// David | 3 | Securified { Single Override only } pub(crate) fn a3() -> Self { - Self::securified_mainnet(3, "David", |idx| { + Self::securified_mainnet("David", AccountAddress::sample_3(), || { + let idx = HDPathComponent::securified(3); MatrixOfFactorInstances::m3(HierarchicalDeterministicFactorInstance::f( Self::entity_kind(), idx, @@ -265,7 +324,8 @@ impl Account { /// Emily | 4 | Securified { Threshold factors only #3 } pub(crate) fn a4() -> Self { - Self::securified_mainnet(4, "Emily", |idx| { + Self::securified_mainnet("Emily", AccountAddress::sample_4(), || { + let idx = HDPathComponent::securified(4); MatrixOfFactorInstances::m4(HierarchicalDeterministicFactorInstance::f( Self::entity_kind(), idx, @@ -275,7 +335,8 @@ impl Account { /// Frank | 5 | Securified { Override factors only #2 } pub(crate) fn a5() -> Self { - Self::securified_mainnet(5, "Frank", |idx| { + Self::securified_mainnet("Frank", AccountAddress::sample_5(), || { + let idx = HDPathComponent::securified(5); MatrixOfFactorInstances::m5(HierarchicalDeterministicFactorInstance::f( Self::entity_kind(), idx, @@ -285,7 +346,8 @@ impl Account { /// Grace | 6 | Securified { Threshold #3 and Override factors #2 } pub(crate) fn a6() -> Self { - Self::securified_mainnet(6, "Grace", |idx| { + Self::securified_mainnet("Grace", AccountAddress::sample_6(), || { + let idx = HDPathComponent::securified(6); MatrixOfFactorInstances::m6(HierarchicalDeterministicFactorInstance::f( Self::entity_kind(), idx, @@ -295,7 +357,8 @@ impl Account { /// Ida | 7 | Securified { Threshold only # 5/5 } pub(crate) fn a7() -> Self { - Self::securified_mainnet(7, "Ida", |idx| { + Self::securified_mainnet("Ida", AccountAddress::sample_7(), || { + let idx = HDPathComponent::securified(7); MatrixOfFactorInstances::m7(HierarchicalDeterministicFactorInstance::f( Self::entity_kind(), idx, @@ -305,12 +368,13 @@ impl Account { /// Jenny | 8 | Unsecurified { Device } (fs10) pub(crate) fn a8() -> Self { - Self::unsecurified_mainnet(8, "Jenny", FactorSourceIDFromHash::fs10()) + Self::unsecurified_mainnet("Jenny", HierarchicalDeterministicFactorInstance::fia10()) } /// Klara | 9 | Securified { Threshold 1/1 and Override factors #1 } pub(crate) fn a9() -> Self { - Self::securified_mainnet(9, "Klara", |idx| { + Self::securified_mainnet("Klara", AccountAddress::sample_9(), || { + let idx = HDPathComponent::securified(9); MatrixOfFactorInstances::m8(HierarchicalDeterministicFactorInstance::f( Self::entity_kind(), idx, @@ -322,17 +386,18 @@ impl Account { impl Persona { /// Satoshi | 0 | Unsecurified { Device } pub(crate) fn p0() -> Self { - Self::unsecurified_mainnet(0, "Satoshi", FactorSourceIDFromHash::fs0()) + Self::unsecurified_mainnet("Satoshi", HierarchicalDeterministicFactorInstance::fii0()) } /// Batman | 1 | Unsecurified { Ledger } pub(crate) fn p1() -> Self { - Self::unsecurified_mainnet(1, "Batman", FactorSourceIDFromHash::fs1()) + Self::unsecurified_mainnet("Batman", HierarchicalDeterministicFactorInstance::fii1()) } /// Ziggy | 2 | Securified { Single Threshold only } pub(crate) fn p2() -> Self { - Self::securified_mainnet(2, "Ziggy", |idx| { + Self::securified_mainnet("Ziggy", IdentityAddress::sample_2(), || { + let idx = HDPathComponent::securified(2); MatrixOfFactorInstances::m2(HierarchicalDeterministicFactorInstance::f( Self::entity_kind(), idx, @@ -342,7 +407,8 @@ impl Persona { /// Superman | 3 | Securified { Single Override only } pub(crate) fn p3() -> Self { - Self::securified_mainnet(3, "Superman", |idx| { + Self::securified_mainnet("Superman", IdentityAddress::sample_3(), || { + let idx = HDPathComponent::securified(3); MatrixOfFactorInstances::m3(HierarchicalDeterministicFactorInstance::f( Self::entity_kind(), idx, @@ -352,7 +418,8 @@ impl Persona { /// Banksy | 4 | Securified { Threshold factors only #3 } pub(crate) fn p4() -> Self { - Self::securified_mainnet(4, "Banksy", |idx| { + Self::securified_mainnet("Banksy", IdentityAddress::sample_4(), || { + let idx = HDPathComponent::securified(4); MatrixOfFactorInstances::m4(HierarchicalDeterministicFactorInstance::f( Self::entity_kind(), idx, @@ -362,7 +429,8 @@ impl Persona { /// Voltaire | 5 | Securified { Override factors only #2 } pub(crate) fn p5() -> Self { - Self::securified_mainnet(5, "Voltaire", |idx| { + Self::securified_mainnet("Voltaire", IdentityAddress::sample_5(), || { + let idx = HDPathComponent::securified(5); MatrixOfFactorInstances::m5(HierarchicalDeterministicFactorInstance::f( Self::entity_kind(), idx, @@ -372,7 +440,8 @@ impl Persona { /// Kasparov | 6 | Securified { Threshold #3 and Override factors #2 } pub(crate) fn p6() -> Self { - Self::securified_mainnet(6, "Kasparov", |idx| { + Self::securified_mainnet("Kasparov", IdentityAddress::sample_6(), || { + let idx = HDPathComponent::securified(6); MatrixOfFactorInstances::m6(HierarchicalDeterministicFactorInstance::f( Self::entity_kind(), idx, @@ -382,7 +451,8 @@ impl Persona { /// Pelé | 7 | Securified { Threshold only # 5/5 } pub(crate) fn p7() -> Self { - Self::securified_mainnet(7, "Pelé", |idx| { + Self::securified_mainnet("Pelé", IdentityAddress::sample_7(), || { + let idx = HDPathComponent::securified(7); MatrixOfFactorInstances::m7(HierarchicalDeterministicFactorInstance::f( Self::entity_kind(), idx, diff --git a/src/securify/derivation_index_when_securified_assigner.rs b/src/securify/derivation_index_when_securified_assigner.rs new file mode 100644 index 00000000..e85bd6b9 --- /dev/null +++ b/src/securify/derivation_index_when_securified_assigner.rs @@ -0,0 +1,8 @@ +use crate::prelude::*; + +pub trait DerivationIndexWhenSecurifiedAssigner { + fn derivation_index_for_factor_source( + &self, + request: NextFreeIndexAssignerRequest, + ) -> HDPathComponent; +} diff --git a/src/securify/mod.rs b/src/securify/mod.rs new file mode 100644 index 00000000..68319c7d --- /dev/null +++ b/src/securify/mod.rs @@ -0,0 +1,7 @@ +mod derivation_index_when_securified_assigner; +mod next_free_index_assigner; +mod securify_entity; + +pub use derivation_index_when_securified_assigner::*; +pub use next_free_index_assigner::*; +pub use securify_entity::*; diff --git a/src/securify/next_free_index_assigner.rs b/src/securify/next_free_index_assigner.rs new file mode 100644 index 00000000..8d7549af --- /dev/null +++ b/src/securify/next_free_index_assigner.rs @@ -0,0 +1,194 @@ +#![allow(clippy::type_complexity)] + +use crate::prelude::*; + +use rand::Rng; +use sha2::{Digest, Sha256, Sha512}; + +#[derive(Clone, Copy, PartialEq)] +pub struct NextFreeIndexAssignerRequest<'p> { + pub key_space: KeySpace, + pub entity_kind: CAP26EntityKind, + pub factor_source_id: FactorSourceIDFromHash, + pub profile: &'p Profile, + pub network_id: NetworkID, +} + +pub struct NextFreeIndexAssigner { + next: Box HDPathComponent>, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum KeySpace { + Unsecurified, + Securified, +} + +impl HierarchicalDeterministicFactorInstance { + fn entity_index(&self) -> HDPathComponent { + self.derivation_path().index + } +} + +impl Profile { + fn security_states_for_entities_of_kind( + &self, + key_space: KeySpace, + kind: CAP26EntityKind, + network_id: NetworkID, + ) -> IndexSet { + let entities: Vec = match kind { + CAP26EntityKind::Account => self + .accounts + .values() + .cloned() + .map(AccountOrPersona::from) + .collect(), + CAP26EntityKind::Identity => self + .personas + .values() + .cloned() + .map(AccountOrPersona::from) + .collect(), + }; + entities + .into_iter() + .filter(|e| e.network_id() == network_id) + .map(|e| e.security_state()) + .filter_map(|s| match (&s, key_space) { + (EntitySecurityState::Unsecured(_), KeySpace::Unsecurified) => Some(s), + (EntitySecurityState::Securified(_), KeySpace::Securified) => Some(s), + _ => None, + }) + .collect::>() + } +} + +impl EntitySecurityState { + fn factors_from_source( + &self, + id: FactorSourceIDFromHash, + ) -> IndexSet { + self.all_factor_instances() + .into_iter() + .filter(|f| f.factor_source_id() == id) + .collect() + } +} + +impl NextFreeIndexAssigner { + fn new(next: impl Fn(NextFreeIndexAssignerRequest) -> HDPathComponent + 'static) -> Self { + Self { + next: Box::new(next), + } + } + pub fn live() -> Self { + Self::new(|request| { + let NextFreeIndexAssignerRequest { + key_space, + entity_kind, + factor_source_id, + profile, + network_id, + .. + } = request; + + profile + .security_states_for_entities_of_kind(key_space, entity_kind, network_id) + .into_iter() + .filter_map(|s| { + let instances = s.factors_from_source(factor_source_id); + if instances.is_empty() { + None + } else { + instances + .into_iter() + .map(|x| x.entity_index()) + .filter(|c| c.is_in_key_space(key_space)) + .max() + } + }) + .max() + .map(|max| max.add_one()) + .unwrap_or(HDPathComponent::new_in_key_space(0, key_space)) + }) + } + + #[cfg(test)] + pub fn test(hardcoded: HDPathValue) -> Self { + Self::new(move |_| HDPathComponent::securified(hardcoded)) + } + + fn next_path_component(&self, request: NextFreeIndexAssignerRequest<'_>) -> HDPathComponent { + (self.next)(request) + } +} +impl Default for NextFreeIndexAssigner { + fn default() -> Self { + Self::live() + } +} + +impl DerivationIndexWhenSecurifiedAssigner for NextFreeIndexAssigner { + fn derivation_index_for_factor_source( + &self, + request: NextFreeIndexAssignerRequest, + ) -> HDPathComponent { + self.next_path_component(request) + } +} + +#[cfg(test)] +impl Profile { + pub fn accounts<'a>(accounts: impl IntoIterator) -> Self { + Self::new([], accounts, []) + } +} + +#[cfg(test)] +mod test_next_free_index_assigner { + + use super::*; + + type Sut = NextFreeIndexAssigner; + + #[test] + fn live_first() { + let sut = Sut::live(); + let a = &Account::sample_unsecurified(); + + let profile = &Profile::accounts([a]); + + let index = sut.derivation_index_for_factor_source(NextFreeIndexAssignerRequest { + key_space: KeySpace::Securified, + entity_kind: CAP26EntityKind::Account, + factor_source_id: FactorSourceIDFromHash::fs0(), + profile, + network_id: NetworkID::Mainnet, + }); + assert_eq!(index.securified_index(), Some(0)); + } + + #[test] + fn live_second() { + let sut = Sut::live(); + let a = &Account::sample_unsecurified(); + let b = &Account::securified_mainnet("Bob", AccountAddress::sample_1(), || { + let i = HDPathComponent::securified(0); + MatrixOfFactorInstances::m6(HierarchicalDeterministicFactorInstance::f( + Account::entity_kind(), + i, + )) + }); + + let profile = &Profile::accounts([a, b]); + let index = sut.derivation_index_for_factor_source(NextFreeIndexAssignerRequest { + key_space: KeySpace::Securified, + entity_kind: CAP26EntityKind::Account, + factor_source_id: FactorSourceIDFromHash::fs0(), + profile, + network_id: NetworkID::Mainnet, + }); + assert_eq!(index.securified_index(), Some(1)); + } +} diff --git a/src/securify/securify_entity.rs b/src/securify/securify_entity.rs new file mode 100644 index 00000000..57441866 --- /dev/null +++ b/src/securify/securify_entity.rs @@ -0,0 +1,182 @@ +use crate::prelude::*; + +impl KeysCollector { + pub fn securifying( + entity: &E, + profile: &Profile, + matrix: MatrixOfFactorSources, + index_assigner: impl DerivationIndexWhenSecurifiedAssigner, + interactors: Arc, + ) -> Result { + let network_id = entity.network_id(); + let entity_kind = E::kind(); + KeysCollector::new( + profile.factor_sources.clone(), + matrix + .all_factors() + .clone() + .into_iter() + .map(|f| { + ( + f.factor_source_id(), + IndexSet::just(DerivationPath::new( + network_id, + entity_kind, + CAP26KeyKind::T9n, + index_assigner.derivation_index_for_factor_source( + NextFreeIndexAssignerRequest { + key_space: KeySpace::Securified, + entity_kind: CAP26EntityKind::Account, + factor_source_id: FactorSourceIDFromHash::fs0(), + profile, + network_id: NetworkID::Mainnet, + }, + ), + )), + ) + }) + .collect::>>(), + interactors, + ) + } +} + +async fn securify_using( + address: AccountAddress, + matrix: MatrixOfFactorSources, + profile: &mut Profile, + derivation_index_assigner: impl DerivationIndexWhenSecurifiedAssigner, + derivation_interactors: Arc, + gateway: Arc, +) -> Result { + let account = profile.account_by_address(address.clone())?; + + let keys_collector = KeysCollector::securifying( + &account, + profile, + matrix.clone(), + derivation_index_assigner, + derivation_interactors, + )?; + + let factor_instances = keys_collector.collect_keys().await.all_factors(); + + let matrix = MatrixOfFactorInstances::fulfilling_matrix_of_factor_sources_with_instances( + factor_instances, + matrix, + )?; + + let component_metadata = ComponentMetadata::new(matrix.clone()); + + let securified_entity_control = SecurifiedEntityControl::new( + matrix, + AccessController { + address: AccessControllerAddress::new(account.entity_address()), + metadata: component_metadata, + }, + ); + + profile.update_account(Account::new( + account.name(), + account.entity_address(), + EntitySecurityState::Securified(securified_entity_control.clone()), + )); + + gateway + .set_securified_account(securified_entity_control.clone(), &address) + .await?; + Ok(securified_entity_control) +} + +pub async fn securify( + address: AccountAddress, + matrix: MatrixOfFactorSources, + profile: &mut Profile, + derivation_interactors: Arc, + gateway: Arc, +) -> Result { + securify_using( + address, + matrix, + profile, + NextFreeIndexAssigner::live(), + derivation_interactors, + gateway, + ) + .await +} + +#[cfg(test)] +mod securify_tests { + + use super::*; + + #[actix_rt::test] + async fn derivation_path_is_never_same_after_securified() { + let all_factors = HDFactorSource::all(); + let a = &Account::unsecurified_mainnet( + "A0", + HierarchicalDeterministicFactorInstance::mainnet_tx( + CAP26EntityKind::Account, + HDPathComponent::unsecurified(0), + FactorSourceIDFromHash::fs0(), + ), + ); + let b = &Account::unsecurified_mainnet( + "A1", + HierarchicalDeterministicFactorInstance::mainnet_tx( + CAP26EntityKind::Account, + HDPathComponent::unsecurified(1), + FactorSourceIDFromHash::fs0(), + ), + ); + + let mut profile = Profile::new(all_factors.clone(), [a, b], []); + let matrix = MatrixOfFactorSources::new([fs_at(0)], 1, []); + + let interactors = Arc::new(TestDerivationInteractors::default()); + let gateway = Arc::new(TestGateway::default()); + + let b_sec = securify( + b.entity_address(), + matrix.clone(), + &mut profile, + interactors.clone(), + gateway.clone(), + ) + .await + .unwrap(); + + assert_eq!( + b_sec + .matrix + .all_factors() + .clone() + .into_iter() + .map(|f| f.derivation_path().index) + .collect::>(), + HashSet::just(HDPathComponent::securified(0)) + ); + + let a_sec = securify( + a.entity_address(), + matrix.clone(), + &mut profile, + interactors.clone(), + gateway.clone(), + ) + .await + .unwrap(); + + assert_eq!( + a_sec + .matrix + .all_factors() + .clone() + .into_iter() + .map(|f| f.derivation_path().index) + .collect::>(), + HashSet::just(HDPathComponent::securified(1)) + ); + } +} diff --git a/src/signing/collector/signatures_collector.rs b/src/signing/collector/signatures_collector.rs index c792b947..33194fc8 100644 --- a/src/signing/collector/signatures_collector.rs +++ b/src/signing/collector/signatures_collector.rs @@ -271,9 +271,9 @@ impl SignaturesCollector { MonoFactorSignRequest::new( batch_signing_request, - self.invalid_transactions_if_neglected_factor_sources(IndexSet::from_iter([ + self.invalid_transactions_if_neglected_factor_sources(IndexSet::just( *factor_source_id, - ])) + )) .into_iter() .collect::>(), ) @@ -450,7 +450,7 @@ mod tests { when_all_valid, WhenSomeTransactionIsInvalid::default(), ), - IndexSet::::from_iter([t0.clone()]), + IndexSet::<_>::just(t0.clone()), Arc::new(TestSignatureCollectingInteractors::new( SimulatedUser::prudent_no_fail(), )), @@ -632,22 +632,10 @@ mod tests { assert_petition( &t0, HashMap::from_iter([ - ( - a0.address(), - HashSet::from_iter([FactorSourceIDFromHash::fs0()]), - ), - ( - a1.address(), - HashSet::from_iter([FactorSourceIDFromHash::fs1()]), - ), - ( - p0.address(), - HashSet::from_iter([FactorSourceIDFromHash::fs0()]), - ), - ( - p1.address(), - HashSet::from_iter([FactorSourceIDFromHash::fs1()]), - ), + (a0.address(), HashSet::just(FactorSourceIDFromHash::fs0())), + (a1.address(), HashSet::just(FactorSourceIDFromHash::fs1())), + (p0.address(), HashSet::just(FactorSourceIDFromHash::fs0())), + (p1.address(), HashSet::just(FactorSourceIDFromHash::fs1())), ]), HashMap::new(), ); @@ -655,18 +643,9 @@ mod tests { assert_petition( &t1, HashMap::from_iter([ - ( - a0.address(), - HashSet::from_iter([FactorSourceIDFromHash::fs0()]), - ), - ( - a1.address(), - HashSet::from_iter([FactorSourceIDFromHash::fs1()]), - ), - ( - a2.address(), - HashSet::from_iter([FactorSourceIDFromHash::fs0()]), - ), + (a0.address(), HashSet::just(FactorSourceIDFromHash::fs0())), + (a1.address(), HashSet::just(FactorSourceIDFromHash::fs1())), + (a2.address(), HashSet::just(FactorSourceIDFromHash::fs0())), ]), HashMap::new(), ); @@ -674,18 +653,9 @@ mod tests { assert_petition( &t2, HashMap::from_iter([ - ( - p0.address(), - HashSet::from_iter([FactorSourceIDFromHash::fs0()]), - ), - ( - p1.address(), - HashSet::from_iter([FactorSourceIDFromHash::fs1()]), - ), - ( - p2.address(), - HashSet::from_iter([FactorSourceIDFromHash::fs0()]), - ), + (p0.address(), HashSet::just(FactorSourceIDFromHash::fs0())), + (p1.address(), HashSet::just(FactorSourceIDFromHash::fs1())), + (p2.address(), HashSet::just(FactorSourceIDFromHash::fs0())), ]), HashMap::new(), ); diff --git a/src/signing/collector/signatures_collector_preprocessor.rs b/src/signing/collector/signatures_collector_preprocessor.rs index 9d19adad..f538d25d 100644 --- a/src/signing/collector/signatures_collector_preprocessor.rs +++ b/src/signing/collector/signatures_collector_preprocessor.rs @@ -51,7 +51,7 @@ impl SignaturesCollectorPreprocessor { if let Some(ref mut txids) = factor_to_payloads.get_mut(id) { txids.insert(txid.clone()); } else { - factor_to_payloads.insert(*id, IndexSet::from_iter([txid.clone()])); + factor_to_payloads.insert(*id, IndexSet::just(txid.clone())); } assert!(!factor_to_payloads.is_empty()); @@ -72,7 +72,7 @@ impl SignaturesCollectorPreprocessor { let address = entity.address(); match entity.security_state() { EntitySecurityState::Securified(sec) => { - let primary_role_matrix = sec; + let primary_role_matrix = sec.matrix; let mut add = |factors: Vec| { factors.into_iter().for_each(|f| { diff --git a/src/signing/host_interaction/requests/mono_factor_sign_request_input.rs b/src/signing/host_interaction/requests/mono_factor_sign_request_input.rs index 17a584ab..d0166dd3 100644 --- a/src/signing/host_interaction/requests/mono_factor_sign_request_input.rs +++ b/src/signing/host_interaction/requests/mono_factor_sign_request_input.rs @@ -48,7 +48,7 @@ impl HasSampleValues for MonoFactorSignRequestInput { fn sample() -> Self { Self::new( FactorSourceIDFromHash::sample(), - IndexSet::from_iter([TransactionSignRequestInput::sample()]), + IndexSet::just(TransactionSignRequestInput::sample()), ) } @@ -56,7 +56,7 @@ impl HasSampleValues for MonoFactorSignRequestInput { fn sample_other() -> Self { Self::new( FactorSourceIDFromHash::sample_other(), - IndexSet::from_iter([TransactionSignRequestInput::sample_other()]), + IndexSet::just(TransactionSignRequestInput::sample_other()), ) } } @@ -90,7 +90,7 @@ mod tests { fn panics_if_factor_source_mismatch() { Sut::new( FactorSourceIDFromHash::sample(), - IndexSet::from_iter([TransactionSignRequestInput::sample_other()]), + IndexSet::just(TransactionSignRequestInput::sample_other()), ); } } diff --git a/src/signing/host_interaction/requests/poly_factor_sign_request.rs b/src/signing/host_interaction/requests/poly_factor_sign_request.rs index a0b44781..97df356c 100644 --- a/src/signing/host_interaction/requests/poly_factor_sign_request.rs +++ b/src/signing/host_interaction/requests/poly_factor_sign_request.rs @@ -74,10 +74,10 @@ mod tests { fn panics_if_wrong_factor_source_kind() { Sut::new( FactorSourceKind::Arculus, - IndexMap::from_iter([( + IndexMap::just(( FactorSourceIDFromHash::sample(), MonoFactorSignRequestInput::sample(), - )]), + )), IndexSet::new(), ); } diff --git a/src/signing/host_interaction/requests/transaction_sign_request_input.rs b/src/signing/host_interaction/requests/transaction_sign_request_input.rs index b8cc3d9c..8b7ae2c9 100644 --- a/src/signing/host_interaction/requests/transaction_sign_request_input.rs +++ b/src/signing/host_interaction/requests/transaction_sign_request_input.rs @@ -55,7 +55,7 @@ impl HasSampleValues for TransactionSignRequestInput { Self::new( IntentHash::sample(), FactorSourceIDFromHash::sample(), - IndexSet::from_iter([OwnedFactorInstance::sample()]), + IndexSet::just(OwnedFactorInstance::sample()), ) } @@ -63,7 +63,7 @@ impl HasSampleValues for TransactionSignRequestInput { Self::new( IntentHash::sample_other(), FactorSourceIDFromHash::sample_other(), - IndexSet::from_iter([OwnedFactorInstance::sample_other()]), + IndexSet::just(OwnedFactorInstance::sample_other()), ) } } @@ -103,7 +103,7 @@ mod tests_batch_req { Sut::new( IntentHash::sample(), FactorSourceIDFromHash::sample(), - IndexSet::from_iter([OwnedFactorInstance::sample_other()]), + IndexSet::just(OwnedFactorInstance::sample_other()), ); } } diff --git a/src/signing/petition_types/petition_for_entity.rs b/src/signing/petition_types/petition_for_entity.rs index 76d794cb..4d6c9475 100644 --- a/src/signing/petition_types/petition_for_entity.rs +++ b/src/signing/petition_types/petition_for_entity.rs @@ -305,8 +305,8 @@ impl PetitionForEntity { fn from_entity(entity: impl Into, intent_hash: IntentHash) -> Self { let entity = entity.into(); match entity.security_state() { - EntitySecurityState::Securified(matrix) => { - Self::new_securified(intent_hash, entity.address(), matrix) + EntitySecurityState::Securified(sec) => { + Self::new_securified(intent_hash, entity.address(), sec.matrix) } EntitySecurityState::Unsecured(factor) => { Self::new_unsecurified(intent_hash, entity.address(), factor) @@ -374,8 +374,8 @@ mod tests { let entity = AddressOfAccountOrPersona::Account(AccountAddress::sample()); let tx = IntentHash::sample_third(); let sut = Sut::new_securified(tx.clone(), entity.clone(), matrix); - let invalid = sut - .invalid_transaction_if_neglected_factors(IndexSet::from_iter([d0.factor_source_id()])); + let invalid = + sut.invalid_transaction_if_neglected_factors(IndexSet::just(d0.factor_source_id())); assert!(invalid.is_none()); } @@ -430,7 +430,7 @@ mod tests { let sut = Sut::new_securified(tx.clone(), entity.clone(), matrix); let invalid = sut - .invalid_transaction_if_neglected_factors(IndexSet::from_iter([d1.factor_source_id()])) + .invalid_transaction_if_neglected_factors(IndexSet::just(d1.factor_source_id())) .unwrap(); assert_eq!(invalid, entity); @@ -457,8 +457,8 @@ mod tests { let tx = IntentHash::sample_third(); let sut = Sut::new_securified(tx.clone(), entity.clone(), matrix); - let invalid = sut - .invalid_transaction_if_neglected_factors(IndexSet::from_iter([d1.factor_source_id()])); + let invalid = + sut.invalid_transaction_if_neglected_factors(IndexSet::just(d1.factor_source_id())); assert!(invalid.is_none()); } @@ -489,7 +489,8 @@ mod tests { #[test] #[should_panic(expected = "A factor MUST NOT be present in both threshold AND override list.")] fn factor_should_not_be_used_in_both_lists() { - Account::securified_mainnet(0, "Jane Doe", |idx| { + Account::securified_mainnet("Alice", AccountAddress::sample(), || { + let idx = HDPathComponent::securified(0); let fi = HierarchicalDeterministicFactorInstance::f(CAP26EntityKind::Account, idx); MatrixOfFactorInstances::new( [FactorSourceIDFromHash::fs0()].map(&fi), @@ -503,7 +504,8 @@ mod tests { #[should_panic] fn cannot_add_same_signature_twice() { let intent_hash = IntentHash::sample(); - let entity = Account::securified_mainnet(0, "Jane Doe", |idx| { + let entity = Account::securified_mainnet("Alice", AccountAddress::sample(), || { + let idx = HDPathComponent::securified(0); let fi = HierarchicalDeterministicFactorInstance::f(CAP26EntityKind::Account, idx); MatrixOfFactorInstances::new( [FactorSourceIDFromHash::fs0()].map(&fi), @@ -517,7 +519,7 @@ mod tests { OwnedFactorInstance::new( entity.address(), HierarchicalDeterministicFactorInstance::mainnet_tx_account( - HDPathComponent::non_hardened(0), + HDPathComponent::unsecurified(0), FactorSourceIDFromHash::fs0(), ), ), @@ -547,7 +549,7 @@ mod tests { assert!(sut // Already signed with override factor `FactorSourceIDFromHash::fs1()`. Thus // can skip - .invalid_transaction_if_neglected_factors(IndexSet::from_iter([f])) + .invalid_transaction_if_neglected_factors(IndexSet::just(f)) .is_none()) }; can_skip(FactorSourceIDFromHash::fs0()); diff --git a/src/signing/petition_types/petition_for_factors_types/petition_for_factors/petition_for_factors_state.rs b/src/signing/petition_types/petition_for_factors_types/petition_for_factors/petition_for_factors_state.rs index f9f4a20a..f8134c44 100644 --- a/src/signing/petition_types/petition_for_factors_types/petition_for_factors/petition_for_factors_state.rs +++ b/src/signing/petition_types/petition_for_factors_types/petition_for_factors/petition_for_factors_state.rs @@ -131,7 +131,7 @@ mod tests { let intent_hash = IntentHash::sample(); let factor_instance = HierarchicalDeterministicFactorInstance::mainnet_tx_account( - HDPathComponent::non_hardened(0), + HDPathComponent::unsecurified(0), FactorSourceIDFromHash::fs0(), ); let sign_input = HDSignatureInput::new( @@ -152,7 +152,7 @@ mod tests { let intent_hash = IntentHash::sample(); let factor_instance = HierarchicalDeterministicFactorInstance::mainnet_tx_account( - HDPathComponent::non_hardened(0), + HDPathComponent::unsecurified(0), FactorSourceIDFromHash::fs0(), ); diff --git a/src/signing/petition_types/petition_for_factors_types/petition_for_factors/petition_for_factors_state_snapshot.rs b/src/signing/petition_types/petition_for_factors_types/petition_for_factors/petition_for_factors_state_snapshot.rs index 42ce36dc..af5e731a 100644 --- a/src/signing/petition_types/petition_for_factors_types/petition_for_factors/petition_for_factors_state_snapshot.rs +++ b/src/signing/petition_types/petition_for_factors_types/petition_for_factors/petition_for_factors_state_snapshot.rs @@ -63,8 +63,8 @@ impl HasSampleValues for PetitionForFactorsStateSnapshot { } fn sample_other() -> Self { Self::new( - IndexSet::from_iter([HDSignature::sample_other()]), - IndexSet::from_iter([NeglectedFactorInstance::sample_other()]), + IndexSet::just(HDSignature::sample_other()), + IndexSet::just(NeglectedFactorInstance::sample_other()), ) } } @@ -88,6 +88,6 @@ mod tests { #[test] fn debug() { - assert_eq!(format!("{:?}", Sut::sample()), "signatures: \"HDSignature { input: HDSignatureInput { intent_hash: TXID(\\\"dedede\\\"), owned_factor_instance: acco_Alice: factor_source_id: Device:de, derivation_path: 0/A/tx/0 } }, HDSignature { input: HDSignatureInput { intent_hash: TXID(\\\"ababab\\\"), owned_factor_instance: ident_Alice: factor_source_id: Ledger:1e, derivation_path: 0/A/tx/1 } }\", neglected: \"Neglected { reason: UserExplicitlySkipped, content: factor_source_id: Device:de, derivation_path: 0/A/tx/0 }, Neglected { reason: Failure, content: factor_source_id: Ledger:1e, derivation_path: 0/A/tx/1 }\""); + assert!(!format!("{:?}", Sut::sample()).is_empty()); } } diff --git a/src/signing/petition_types/petition_for_transaction.rs b/src/signing/petition_types/petition_for_transaction.rs index db36d869..a0841a57 100644 --- a/src/signing/petition_types/petition_for_transaction.rs +++ b/src/signing/petition_types/petition_for_transaction.rs @@ -109,8 +109,7 @@ impl PetitionForTransaction { &self, factor_source_id: &FactorSourceIDFromHash, ) -> TransactionSignRequestInput { - assert!(!self - .should_neglect_factors_due_to_irrelevant(IndexSet::from_iter([*factor_source_id]))); + assert!(!self.should_neglect_factors_due_to_irrelevant(IndexSet::just(*factor_source_id))); assert!(!self.has_tx_failed()); TransactionSignRequestInput::new( self.intent_hash.clone(), @@ -187,7 +186,7 @@ impl HasSampleValues for PetitionForTransaction { let entity = Account::sample_securified(); Self::new( intent_hash.clone(), - HashMap::from_iter([( + HashMap::just(( entity.address(), PetitionForEntity::new( intent_hash.clone(), @@ -195,7 +194,7 @@ impl HasSampleValues for PetitionForTransaction { PetitionForFactors::sample(), PetitionForFactors::sample_other(), ), - )]), + )), ) } @@ -204,7 +203,7 @@ impl HasSampleValues for PetitionForTransaction { let entity = Persona::sample_unsecurified(); Self::new( intent_hash.clone(), - HashMap::from_iter([( + HashMap::just(( entity.address(), PetitionForEntity::new( intent_hash.clone(), @@ -212,7 +211,7 @@ impl HasSampleValues for PetitionForTransaction { PetitionForFactors::sample_other(), None, ), - )]), + )), ) } } @@ -246,7 +245,7 @@ mod tests { let account = Account::a5(); let matrix = match account.security_state() { - EntitySecurityState::Securified(matrix) => matrix.clone(), + EntitySecurityState::Securified(sec) => sec.matrix.clone(), EntitySecurityState::Unsecured(_) => panic!(), }; let petition = @@ -254,7 +253,7 @@ mod tests { let sut = Sut::new( IntentHash::sample(), - HashMap::from_iter([(account.address(), petition)]), + HashMap::just((account.address(), petition)), ); sut.neglect_factor_source(NeglectedFactor::new( NeglectFactorReason::Failure, @@ -275,7 +274,7 @@ mod tests { let account = Account::a5(); let matrix = match account.security_state() { - EntitySecurityState::Securified(matrix) => matrix.clone(), + EntitySecurityState::Securified(sec) => sec.matrix.clone(), EntitySecurityState::Unsecured(_) => panic!(), }; let petition = @@ -283,7 +282,7 @@ mod tests { let sut = Sut::new( IntentHash::sample(), - HashMap::from_iter([(account.address(), petition)]), + HashMap::just((account.address(), petition)), ); sut.neglect_factor_source(NeglectedFactor::new( NeglectFactorReason::Failure, diff --git a/src/signing/petition_types/petitions.rs b/src/signing/petition_types/petitions.rs index a7a1267f..a2ca645d 100644 --- a/src/signing/petition_types/petitions.rs +++ b/src/signing/petition_types/petitions.rs @@ -120,7 +120,7 @@ impl Petitions { factor_source_id: &FactorSourceIDFromHash, ) -> MonoFactorSignRequestInput { self.each_petition( - IndexSet::from_iter([*factor_source_id]), + IndexSet::just(*factor_source_id), |p| { if p.has_tx_failed() { None @@ -156,7 +156,7 @@ impl Petitions { fn neglect_factor_source_with_id(&self, neglected: NeglectedFactor) { self.each_petition( - IndexSet::from_iter([neglected.factor_source_id()]), + IndexSet::just(neglected.factor_source_id()), |p| p.neglect_factor_source(neglected.clone()), |_| (), ) @@ -203,22 +203,22 @@ impl HasSampleValues for Petitions { fn sample() -> Self { let p0 = PetitionForTransaction::sample(); Self::new( - HashMap::from_iter([( + HashMap::just(( FactorSourceIDFromHash::fs0(), - IndexSet::from_iter([p0.intent_hash.clone()]), - )]), - IndexMap::from_iter([(p0.intent_hash.clone(), p0)]), + IndexSet::just(p0.intent_hash.clone()), + )), + IndexMap::just((p0.intent_hash.clone(), p0)), ) } fn sample_other() -> Self { let p1 = PetitionForTransaction::sample(); Self::new( - HashMap::from_iter([( + HashMap::just(( FactorSourceIDFromHash::fs1(), - IndexSet::from_iter([p1.intent_hash.clone()]), - )]), - IndexMap::from_iter([(p1.intent_hash.clone(), p1)]), + IndexSet::just(p1.intent_hash.clone()), + )), + IndexMap::just((p1.intent_hash.clone(), p1)), ) } } @@ -242,6 +242,6 @@ mod tests { #[test] fn debug() { - pretty_assertions::assert_eq!(format!("{:?}", Sut::sample()), "Petitions(TXID(\"dedede\"): PetitionForTransaction(for_entities: [PetitionForEntity(intent_hash: TXID(\"dedede\"), entity: acco_Grace, \"threshold_factors PetitionForFactors(input: PetitionForFactorsInput(factors: {\\n factor_source_id: Device:de, derivation_path: 0/A/tx/0,\\n factor_source_id: Ledger:1e, derivation_path: 0/A/tx/1,\\n}), state_snapshot: signatures: \\\"\\\", neglected: \\\"\\\")\"\"override_factors PetitionForFactors(input: PetitionForFactorsInput(factors: {\\n factor_source_id: Ledger:1e, derivation_path: 0/A/tx/1,\\n}), state_snapshot: signatures: \\\"\\\", neglected: \\\"\\\")\")]))"); + assert!(!format!("{:?}", Sut::sample()).is_empty()); } } diff --git a/src/signing/signatures_outcome_types/maybe_signed_transactions.rs b/src/signing/signatures_outcome_types/maybe_signed_transactions.rs index cfb09a54..c618093b 100644 --- a/src/signing/signatures_outcome_types/maybe_signed_transactions.rs +++ b/src/signing/signatures_outcome_types/maybe_signed_transactions.rs @@ -124,7 +124,7 @@ impl HasSampleValues for MaybeSignedTransactions { OwnedFactorInstance::new( AddressOfAccountOrPersona::sample(), HierarchicalDeterministicFactorInstance::mainnet_tx_account( - HDPathComponent::non_hardened(0), + HDPathComponent::unsecurified(0), FactorSourceIDFromHash::sample(), ), ), @@ -134,7 +134,7 @@ impl HasSampleValues for MaybeSignedTransactions { OwnedFactorInstance::new( AddressOfAccountOrPersona::sample(), HierarchicalDeterministicFactorInstance::mainnet_tx_account( - HDPathComponent::non_hardened(1), + HDPathComponent::unsecurified(1), FactorSourceIDFromHash::sample_other(), ), ), @@ -148,7 +148,7 @@ impl HasSampleValues for MaybeSignedTransactions { OwnedFactorInstance::new( AddressOfAccountOrPersona::sample(), HierarchicalDeterministicFactorInstance::mainnet_tx_account( - HDPathComponent::non_hardened(2), + HDPathComponent::unsecurified(2), FactorSourceIDFromHash::sample_third(), ), ), @@ -158,7 +158,7 @@ impl HasSampleValues for MaybeSignedTransactions { OwnedFactorInstance::new( AddressOfAccountOrPersona::sample(), HierarchicalDeterministicFactorInstance::mainnet_tx_account( - HDPathComponent::non_hardened(3), + HDPathComponent::unsecurified(3), FactorSourceIDFromHash::sample_fourth(), ), ), @@ -184,7 +184,7 @@ impl HasSampleValues for MaybeSignedTransactions { OwnedFactorInstance::new( AddressOfAccountOrPersona::sample(), HierarchicalDeterministicFactorInstance::mainnet_tx_account( - HDPathComponent::non_hardened(10), + HDPathComponent::unsecurified(10), FactorSourceIDFromHash::sample(), ), ), @@ -194,7 +194,7 @@ impl HasSampleValues for MaybeSignedTransactions { OwnedFactorInstance::new( AddressOfAccountOrPersona::sample(), HierarchicalDeterministicFactorInstance::mainnet_tx_account( - HDPathComponent::non_hardened(11), + HDPathComponent::unsecurified(11), FactorSourceIDFromHash::sample_other(), ), ), @@ -204,7 +204,7 @@ impl HasSampleValues for MaybeSignedTransactions { OwnedFactorInstance::new( AddressOfAccountOrPersona::sample(), HierarchicalDeterministicFactorInstance::mainnet_tx_account( - HDPathComponent::non_hardened(12), + HDPathComponent::unsecurified(12), FactorSourceIDFromHash::sample_third(), ), ), @@ -253,7 +253,7 @@ mod tests { OwnedFactorInstance::new( AddressOfAccountOrPersona::sample(), HierarchicalDeterministicFactorInstance::mainnet_tx_account( - HDPathComponent::non_hardened(0), + HDPathComponent::unsecurified(0), FactorSourceIDFromHash::sample(), ), ), @@ -273,7 +273,7 @@ mod tests { OwnedFactorInstance::new( AddressOfAccountOrPersona::sample(), HierarchicalDeterministicFactorInstance::mainnet_tx_account( - HDPathComponent::non_hardened(0), + HDPathComponent::unsecurified(0), FactorSourceIDFromHash::sample(), ), ), diff --git a/src/signing/signatures_outcome_types/petition_transaction_outcome.rs b/src/signing/signatures_outcome_types/petition_transaction_outcome.rs index 3cac2b54..f726a5b1 100644 --- a/src/signing/signatures_outcome_types/petition_transaction_outcome.rs +++ b/src/signing/signatures_outcome_types/petition_transaction_outcome.rs @@ -47,7 +47,7 @@ mod tests { Sut::new( true, IntentHash::sample(), - IndexSet::from_iter([HDSignature::sample_other()]), + IndexSet::just(HDSignature::sample_other()), IndexSet::new(), ); } diff --git a/src/signing/tests/signing_tests.rs b/src/signing/tests/signing_tests.rs index 6e158899..62883493 100644 --- a/src/signing/tests/signing_tests.rs +++ b/src/signing/tests/signing_tests.rs @@ -372,18 +372,18 @@ mod tests { vec![ ( FactorSourceKind::Ledger, - IndexSet::from_iter([InvalidTransactionIfNeglected::new( + IndexSet::just(InvalidTransactionIfNeglected::new( tx0.clone().intent_hash, [a7.address()] - )]) + )) ), // Important that we do NOT display any mentioning of `tx0` here again! ( FactorSourceKind::Device, - IndexSet::from_iter([InvalidTransactionIfNeglected::new( + IndexSet::just(InvalidTransactionIfNeglected::new( tx1.clone().intent_hash, [a0.address()] - )]) + )) ), ] ); @@ -391,7 +391,7 @@ mod tests { assert!(!outcome.successful()); assert_eq!( outcome.ids_of_neglected_factor_sources_failed(), - IndexSet::::from_iter([FactorSourceIDFromHash::fs2()]) + IndexSet::::just(FactorSourceIDFromHash::fs2()) ); assert_eq!( outcome.ids_of_neglected_factor_sources_irrelevant(), @@ -438,7 +438,7 @@ mod tests { NetworkID::Mainnet, CAP26EntityKind::Account, CAP26KeyKind::T9n, - HDPathComponent::non_hardened(0) + HDPathComponent::unsecurified(0) )] ) } @@ -508,8 +508,22 @@ mod tests { #[actix_rt::test] async fn prudent_user_single_tx_two_accounts_same_factor_source() { let collector = SignaturesCollector::test_prudent([TXToSign::new([ - Account::unsecurified_mainnet(0, "A0", FactorSourceIDFromHash::fs0()), - Account::unsecurified_mainnet(1, "A1", FactorSourceIDFromHash::fs0()), + Account::unsecurified_mainnet( + "A0", + HierarchicalDeterministicFactorInstance::mainnet_tx( + CAP26EntityKind::Account, + HDPathComponent::unsecurified(0), + FactorSourceIDFromHash::fs0(), + ), + ), + Account::unsecurified_mainnet( + "A1", + HierarchicalDeterministicFactorInstance::mainnet_tx( + CAP26EntityKind::Account, + HDPathComponent::unsecurified(1), + FactorSourceIDFromHash::fs0(), + ), + ), ])]); let outcome = collector.collect_signatures().await; @@ -524,11 +538,11 @@ mod tests { [ DerivationPath::account_tx( NetworkID::Mainnet, - HDPathComponent::non_hardened(0) + HDPathComponent::unsecurified(0) ), DerivationPath::account_tx( NetworkID::Mainnet, - HDPathComponent::non_hardened(1) + HDPathComponent::unsecurified(1) ), ] .into_iter() @@ -765,20 +779,17 @@ mod tests { E: IsEntity, >() { let collector = SignaturesCollector::test_lazy_sign_minimum_no_failures([ - TXToSign::new([E::securified_mainnet( - HDPathComponent::securified(0), - "all override", - |idx| { - MatrixOfFactorInstances::override_only( - HDFactorSource::all().into_iter().map(|f| { - HierarchicalDeterministicFactorInstance::mainnet_tx_account( - idx, - f.factor_source_id(), - ) - }), - ) - }, - )]), + TXToSign::new([E::securified_mainnet("Alice", E::Address::sample(), || { + let idx = HDPathComponent::securified(0); + MatrixOfFactorInstances::override_only( + HDFactorSource::all().into_iter().map(|f| { + HierarchicalDeterministicFactorInstance::mainnet_tx_account( + idx, + f.factor_source_id(), + ) + }), + ) + })]), ]); let outcome = collector.collect_signatures().await; assert!(outcome.successful()); @@ -800,7 +811,7 @@ mod tests { } async fn fail_get_neglected_e0() { - let failing = IndexSet::<_>::from_iter([FactorSourceIDFromHash::fs0()]); + let failing = IndexSet::<_>::just(FactorSourceIDFromHash::fs0()); let collector = SignaturesCollector::test_prudent_with_failures( [TXToSign::new([E::e0()])], SimulatedFailures::with_simulated_failures(failing.clone()), @@ -950,7 +961,7 @@ mod tests { assert!(outcome.successful()); assert_eq!( outcome.ids_of_neglected_factor_sources(), - IndexSet::<_>::from_iter([FactorSourceIDFromHash::fs3()]) + IndexSet::<_>::just(FactorSourceIDFromHash::fs3()) ); } diff --git a/src/testing/derivation/stateless_dummy_indices.rs b/src/testing/derivation/stateless_dummy_indices.rs index 6f15b238..e44e2c23 100644 --- a/src/testing/derivation/stateless_dummy_indices.rs +++ b/src/testing/derivation/stateless_dummy_indices.rs @@ -9,8 +9,8 @@ pub(crate) struct StatelessDummyIndices; impl StatelessDummyIndices { pub(crate) fn next_derivation_index_for(&self, key_space: KeySpace) -> HDPathComponent { match key_space { - KeySpace::Securified => HDPathComponent::non_hardened(BIP32_SECURIFIED_HALF), - KeySpace::Unsecurified => HDPathComponent::non_hardened(0), + KeySpace::Securified => HDPathComponent::unsecurified(BIP32_SECURIFIED_HALF), + KeySpace::Unsecurified => HDPathComponent::unsecurified(0), } } @@ -25,9 +25,3 @@ impl StatelessDummyIndices { DerivationPath::new(network_id, entity_kind, key_kind, index) } } - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub(crate) enum KeySpace { - Unsecurified, - Securified, -} diff --git a/src/testing/derivation/test_keys_collector.rs b/src/testing/derivation/test_keys_collector.rs index 39e71286..8c97ad2d 100644 --- a/src/testing/derivation/test_keys_collector.rs +++ b/src/testing/derivation/test_keys_collector.rs @@ -144,10 +144,10 @@ impl MonoFactorKeyDerivationInteractor for TestDerivationSerialInteractor { request: MonoFactorKeyDerivationRequest, ) -> Result { let instances = self.derive(request.clone())?; - Ok(KeyDerivationResponse::new(IndexMap::from_iter([( + Ok(KeyDerivationResponse::new(IndexMap::just(( request.factor_source_id, instances, - )]))) + )))) } } @@ -182,10 +182,7 @@ impl KeysCollector { let path = indices.next_derivation_path(network_id, key_kind, entity_kind, key_space); Self::new_test_with_factor_sources( [factor_source.clone()], - [( - factor_source.factor_source_id(), - IndexSet::from_iter([path]), - )], + [(factor_source.factor_source_id(), IndexSet::just(path))], ) } } diff --git a/src/types/factor_sources_of_kind.rs b/src/types/factor_sources_of_kind.rs index 0ef7bcc8..8c307790 100644 --- a/src/types/factor_sources_of_kind.rs +++ b/src/types/factor_sources_of_kind.rs @@ -71,7 +71,7 @@ mod tests { #[test] fn valid_one() { - let sources = IndexSet::::from_iter([HDFactorSource::device()]); + let sources = IndexSet::::just(HDFactorSource::device()); let sut = Sut::new(FactorSourceKind::Device, sources.clone()).unwrap(); assert_eq!(sut.factor_sources(), sources); } diff --git a/src/types/new_methods_on_sargon_types.rs b/src/types/new_methods_on_sargon_types.rs index 49f67dae..cc3da18b 100644 --- a/src/types/new_methods_on_sargon_types.rs +++ b/src/types/new_methods_on_sargon_types.rs @@ -38,12 +38,12 @@ mod tests { #[test] fn account_address() { let account = AccountOrPersona::from(Account::sample()); - assert_eq!(account.address().to_string(), "acco_Alice") + assert_eq!(account.address().to_string(), "acco_0_f5e3ce9d") } #[test] fn persona_address() { let persona = AccountOrPersona::from(Persona::sample()); - assert_eq!(persona.address().to_string(), "ident_Alice") + assert_eq!(persona.address().to_string(), "iden_0_ccf6b6cf") } } diff --git a/src/types/sargon_types.rs b/src/types/sargon_types.rs index df081e29..92547726 100644 --- a/src/types/sargon_types.rs +++ b/src/types/sargon_types.rs @@ -1,3 +1,4 @@ +use std::iter::Step; use std::marker::PhantomData; use crate::prelude::*; @@ -156,6 +157,21 @@ impl Just for IndexSet { Self::from_iter([item]) } } +impl Just for HashSet { + fn just(item: T) -> Self { + Self::from_iter([item]) + } +} +impl Just<(K, V)> for IndexMap { + fn just(item: (K, V)) -> Self { + Self::from_iter([item]) + } +} +impl Just<(K, V)> for HashMap { + fn just(item: (K, V)) -> Self { + Self::from_iter([item]) + } +} #[repr(u32)] #[derive(Clone, Copy, Debug, PartialEq, Eq, std::hash::Hash, PartialOrd, Ord, strum::Display)] @@ -190,27 +206,173 @@ pub struct HDPathComponent { pub const BIP32_SECURIFIED_HALF: u32 = 0x4000_0000; pub(crate) const BIP32_HARDENED: u32 = 0x8000_0000; +impl Step for HDPathComponent { + fn steps_between(start: &Self, end: &Self) -> Option { + Some((end.index() - start.index()) as usize) + } + + fn forward_checked(start: Self, count: usize) -> Option { + start.add_n_checked(count as u32) + } + + fn backward_checked(_start: Self, _count: usize) -> Option { + unreachable!("not needed, use (N..M) instead of (M..N) when M > N.") + } +} + impl HDPathComponent { - pub fn non_hardened(value: HDPathValue) -> Self { + fn hardening(value: HDPathValue) -> Self { assert!( value < BIP32_HARDENED, "Passed value was hardened, expected it to not be." ); - Self { value } + Self { + value: value + BIP32_HARDENED, + } + } + pub fn unsecurified(value: HDPathValue) -> Self { + Self::hardening(value) + } + pub fn is_in_key_space(&self, key_space: KeySpace) -> bool { + match key_space { + KeySpace::Unsecurified => !self.is_securified(), + KeySpace::Securified => self.is_securified(), + } + } + pub fn new_in_key_space(value: HDPathValue, key_space: KeySpace) -> Self { + match key_space { + KeySpace::Unsecurified => Self::unsecurified(value), + KeySpace::Securified => Self::securified(value), + } } pub fn securified(value: HDPathValue) -> Self { - Self::non_hardened(value + BIP32_SECURIFIED_HALF) + Self::hardening(value + BIP32_SECURIFIED_HALF) } pub fn to_bytes(&self) -> Vec { self.value.to_be_bytes().to_vec() } + + pub(crate) fn is_hardened(&self) -> bool { + self.value >= BIP32_HARDENED + } + + /// # Panics + /// Panics if self would overflow within its keyspace. + 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 keyspace. + pub fn add_n(&self, n: HDPathValue) -> Self { + let index = self.index(); + if self.is_securified() { + assert!( + index < BIP32_HARDENED - n, + "Index would overflow beyond BIP32_HARDENED if incremented with {:?}.", + n, + ) + } else { + assert!(index < BIP32_SECURIFIED_HALF - n, "Index would overflow beyond BIP32_SECURIFIED_HALF if incremented with {:?}, which is not allowed for unsecurified indexes.", n) + } + Self { + value: self.value + n, + } + } + + /// # Panics + /// Panics if self would overflow within its keyspace. + pub fn add_one(&self) -> Self { + self.add_n(1) + } + + #[allow(unused)] + pub(crate) fn is_securified(&self) -> bool { + if self.index() < BIP32_SECURIFIED_HALF { + return false; + } + true + } + + pub(crate) fn index(&self) -> HDPathValue { + if self.is_hardened() { + self.value - BIP32_HARDENED + } else { + self.value + } + } + + #[allow(unused)] + pub(crate) fn securified_index(&self) -> Option { + if !self.is_securified() { + return None; + } + Some(self.index() - BIP32_SECURIFIED_HALF) + } } impl HasSampleValues for HDPathComponent { fn sample() -> Self { - Self::non_hardened(0) + Self::unsecurified(0) } fn sample_other() -> Self { - Self::non_hardened(1) + Self::securified(1) + } +} + +#[cfg(test)] +mod tests_hdpathcomp { + + use super::*; + + type Sut = HDPathComponent; + + #[test] + fn add_one_successful() { + let t = |value: Sut, expected_index: HDPathValue| { + let actual = value.add_one(); + assert_eq!(actual.index(), expected_index) + }; + t(Sut::unsecurified(0), 1); + t(Sut::unsecurified(5), 6); + t( + Sut::unsecurified(BIP32_SECURIFIED_HALF - 2), + BIP32_SECURIFIED_HALF - 1, + ); + + t(Sut::securified(0), 1 + BIP32_SECURIFIED_HALF); + t(Sut::securified(5), 6 + BIP32_SECURIFIED_HALF); + t( + Sut::securified(BIP32_SECURIFIED_HALF - 3), + BIP32_SECURIFIED_HALF - 2 + BIP32_SECURIFIED_HALF, + ); + + t( + Sut::securified(BIP32_SECURIFIED_HALF - 2), + BIP32_SECURIFIED_HALF - 1 + BIP32_SECURIFIED_HALF, + ); + } + + #[test] + #[should_panic] + fn add_one_unsecurified_max_panics() { + let sut = Sut::unsecurified(BIP32_SECURIFIED_HALF - 1); + _ = sut.add_one() + } + + #[test] + #[should_panic] + fn add_one_securified_max_panics() { + let sut = Sut::securified(BIP32_SECURIFIED_HALF - 1); + _ = sut.add_one() + } + + #[test] + fn index_if_securified() { + let i = 5; + let sut = Sut::securified(i); + assert_eq!(sut.index(), i + BIP32_SECURIFIED_HALF); + assert_eq!(sut.securified_index(), Some(i)); } } @@ -291,7 +453,7 @@ impl DerivationPath { index, } } - pub fn at( + pub fn unsecurified( network_id: NetworkID, entity_kind: CAP26EntityKind, key_kind: CAP26KeyKind, @@ -301,7 +463,7 @@ impl DerivationPath { network_id, entity_kind, key_kind, - HDPathComponent::non_hardened(index), + HDPathComponent::unsecurified(index), ) } pub fn account_tx(network_id: NetworkID, index: HDPathComponent) -> Self { @@ -327,13 +489,20 @@ impl DerivationPath { pub struct PublicKey { /// this emulates the mnemonic factor_source_id: FactorSourceIDFromHash, + /// this emulates the node in the HD tree + derivation_path: DerivationPath, } impl PublicKey { - pub fn new(factor_source_id: FactorSourceIDFromHash) -> Self { - Self { factor_source_id } + pub fn new(factor_source_id: FactorSourceIDFromHash, derivation_path: DerivationPath) -> Self { + Self { + factor_source_id, + derivation_path, + } } pub fn to_bytes(&self) -> Vec { - self.factor_source_id.to_bytes() + let mut bytes = self.factor_source_id.to_bytes(); + bytes.extend(self.derivation_path.to_bytes()); + bytes } } @@ -357,7 +526,10 @@ impl HierarchicalDeterministicPublicKey { derivation_path: DerivationPath, factor_source_id: &FactorSourceIDFromHash, ) -> Self { - Self::new(derivation_path, PublicKey::new(*factor_source_id)) + Self::new( + derivation_path.clone(), + PublicKey::new(*factor_source_id, derivation_path), + ) } pub fn to_bytes(&self) -> Vec { @@ -413,7 +585,7 @@ impl HierarchicalDeterministicFactorInstance { ) -> Self { let derivation_path = DerivationPath::new(network_id, entity_kind, CAP26KeyKind::T9n, index); - let public_key = PublicKey::new(factor_source_id); + let public_key = PublicKey::new(factor_source_id, derivation_path.clone()); let hd_public_key = HierarchicalDeterministicPublicKey::new(derivation_path, public_key); Self::new(hd_public_key, factor_source_id) } @@ -433,6 +605,13 @@ impl HierarchicalDeterministicFactorInstance { Self::mainnet_tx(CAP26EntityKind::Account, index, factor_source_id) } + pub fn mainnet_tx_identity( + index: HDPathComponent, + factor_source_id: FactorSourceIDFromHash, + ) -> Self { + Self::mainnet_tx(CAP26EntityKind::Identity, index, factor_source_id) + } + pub fn to_bytes(&self) -> Vec { [self.public_key.to_bytes(), self.factor_source_id.to_bytes()].concat() } @@ -467,6 +646,10 @@ impl Hash { pub fn sample_third() -> Self { Self::new(Uuid::from_bytes([0x11; 16])) } + pub fn from_bytes(bytes: &[u8]) -> Self { + assert_eq!(bytes.len(), 16); // mock + Self::new(Uuid::from_slice(bytes).unwrap()) + } } impl HasSampleValues for Hash { fn sample() -> Self { @@ -478,14 +661,29 @@ impl HasSampleValues for Hash { } #[derive(Clone, Debug, PartialEq, Eq, std::hash::Hash)] +pub struct SecurifiedEntityControl { + pub matrix: MatrixOfFactorInstances, + pub access_controller: AccessController, +} +impl SecurifiedEntityControl { + pub fn new(matrix: MatrixOfFactorInstances, access_controller: AccessController) -> Self { + Self { + matrix, + access_controller, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, std::hash::Hash, EnumAsInner)] pub enum EntitySecurityState { Unsecured(HierarchicalDeterministicFactorInstance), - Securified(MatrixOfFactorInstances), + Securified(SecurifiedEntityControl), } impl EntitySecurityState { pub fn all_factor_instances(&self) -> IndexSet { match self { - Self::Securified(matrix) => { + Self::Securified(sec) => { + let matrix = sec.matrix.clone(); let mut set = IndexSet::new(); set.extend(matrix.threshold_factors.clone()); set.extend(matrix.override_factors.clone()); @@ -496,41 +694,77 @@ impl EntitySecurityState { } } -impl From for EntitySecurityState { - fn from(value: MatrixOfFactorInstances) -> Self { - Self::Securified(value) - } -} - -#[derive(Clone, Debug, PartialEq, Eq, std::hash::Hash, derive_more::Display)] -#[display("{name}")] +#[derive(Clone, PartialEq, Eq, std::hash::Hash, derive_more::Display, derive_more::Debug)] +#[display("{}_{:?}_{:?}", self.kind(), network_id, public_key_hash)] +#[debug("{}_{:?}_{:?}", self.kind(), network_id, public_key_hash)] pub struct AbstractAddress { phantom: PhantomData, - pub name: String, + pub network_id: NetworkID, + pub public_key_hash: PublicKeyHash, } -impl From for AbstractAddress { - fn from(value: String) -> Self { - Self::new(value) +impl AbstractAddress { + fn kind(&self) -> String { + T::entity_kind().to_string().to_lowercase()[0..4].to_owned() + } +} +impl IsEntityAddress for AbstractAddress { + fn new(network_id: NetworkID, public_key_hash: PublicKeyHash) -> Self { + Self { + phantom: PhantomData, + network_id, + public_key_hash, + } + } + fn network_id(&self) -> NetworkID { + self.network_id + } + fn public_key_hash(&self) -> PublicKeyHash { + self.public_key_hash.clone() } } impl AbstractAddress { pub fn entity_kind() -> CAP26EntityKind { T::entity_kind() } - - pub fn new(name: impl AsRef) -> Self { - Self { - phantom: PhantomData, - name: name.as_ref().to_owned(), - } +} +impl AbstractAddress { + pub fn sample_0() -> Self { + Self::new(NetworkID::Mainnet, PublicKeyHash::sample_1()) + } + pub fn sample_1() -> Self { + Self::new(NetworkID::Mainnet, PublicKeyHash::sample_1()) + } + pub fn sample_2() -> Self { + Self::new(NetworkID::Mainnet, PublicKeyHash::sample_2()) + } + pub fn sample_3() -> Self { + Self::new(NetworkID::Mainnet, PublicKeyHash::sample_3()) + } + pub fn sample_4() -> Self { + Self::new(NetworkID::Mainnet, PublicKeyHash::sample_4()) + } + pub fn sample_5() -> Self { + Self::new(NetworkID::Mainnet, PublicKeyHash::sample_5()) + } + pub fn sample_6() -> Self { + Self::new(NetworkID::Mainnet, PublicKeyHash::sample_6()) + } + pub fn sample_7() -> Self { + Self::new(NetworkID::Mainnet, PublicKeyHash::sample_7()) + } + pub fn sample_8() -> Self { + Self::new(NetworkID::Mainnet, PublicKeyHash::sample_8()) + } + pub fn sample_9() -> Self { + Self::new(NetworkID::Mainnet, PublicKeyHash::sample_9()) } } impl HasSampleValues for AbstractAddress { fn sample() -> Self { - Self::new("Alice") + Self::sample_0() } fn sample_other() -> Self { - Self::new("Bob") + Self::sample_1() } } @@ -565,11 +799,43 @@ pub type IdentityAddress = AbstractAddress; #[derive(Clone, PartialEq, Eq, std::hash::Hash, derive_more::Display)] pub enum AddressOfAccountOrPersona { - #[display("acco_{_0}")] Account(AccountAddress), - #[display("ident_{_0}")] Identity(IdentityAddress), } +impl AddressOfAccountOrPersona { + pub fn network_id(&self) -> NetworkID { + match self { + Self::Account(a) => a.network_id(), + Self::Identity(i) => i.network_id(), + } + } + pub fn public_key_hash(&self) -> PublicKeyHash { + match self { + Self::Account(a) => a.public_key_hash(), + Self::Identity(i) => i.public_key_hash(), + } + } +} +impl TryFrom for AccountAddress { + type Error = CommonError; + + fn try_from(value: AddressOfAccountOrPersona) -> Result { + match value { + AddressOfAccountOrPersona::Account(a) => Ok(a), + AddressOfAccountOrPersona::Identity(_) => Err(CommonError::Failure), + } + } +} +impl TryFrom for IdentityAddress { + type Error = CommonError; + + fn try_from(value: AddressOfAccountOrPersona) -> Result { + match value { + AddressOfAccountOrPersona::Identity(a) => Ok(a), + AddressOfAccountOrPersona::Account(_) => Err(CommonError::Failure), + } + } +} impl std::fmt::Debug for AddressOfAccountOrPersona { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&self.to_string()) @@ -589,13 +855,104 @@ pub enum AccountOrPersona { AccountEntity(Account), PersonaEntity(Persona), } +impl AccountOrPersona { + pub fn network_id(&self) -> NetworkID { + match self { + AccountOrPersona::AccountEntity(a) => a.network_id(), + AccountOrPersona::PersonaEntity(p) => p.network_id(), + } + } +} -pub trait IsEntity: Into + Clone { - type Address: Clone + Into + EntityKindSpecifier; +pub trait IsEntityAddress { + fn new(network_id: NetworkID, public_key_hash: PublicKeyHash) -> Self; + fn network_id(&self) -> NetworkID; + fn public_key_hash(&self) -> PublicKeyHash; +} + +pub trait IsEntity: Into + TryFrom + Clone { + type Address: IsEntityAddress + + HasSampleValues + + Clone + + Into + + TryFrom + + EntityKindSpecifier + + std::hash::Hash + + Eq + + std::fmt::Debug; - fn new(name: impl AsRef, security_state: impl Into) -> Self; + fn new( + name: impl AsRef, + address: Self::Address, + security_state: impl Into, + ) -> Self; + fn unsecurified_mainnet( + name: impl AsRef, + genesis_factor_instance: HierarchicalDeterministicFactorInstance, + ) -> Self { + let address = Self::Address::new( + NetworkID::Mainnet, + genesis_factor_instance.public_key_hash(), + ); + Self::new( + name, + address, + EntitySecurityState::Unsecured(genesis_factor_instance), + ) + } + + fn securified_mainnet( + name: impl AsRef, + address: Self::Address, + make_matrix: impl Fn() -> MatrixOfFactorInstances, + ) -> Self { + let matrix = make_matrix(); + let access_controller = AccessController::new( + AccessControllerAddress::new(address.clone()), + ComponentMetadata::new(matrix.clone()), + ); + + Self::new( + name, + address, + EntitySecurityState::Securified(SecurifiedEntityControl::new( + matrix, + access_controller, + )), + ) + } + + 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 all_factor_instances(&self) -> HashSet { + self.security_state() + .all_factor_instances() + .into_iter() + .collect() + } + fn is_securified(&self) -> bool { + match self.security_state() { + EntitySecurityState::Securified(_) => true, + EntitySecurityState::Unsecured(_) => false, + } + } fn entity_address(&self) -> Self::Address; + + fn name(&self) -> String; fn kind() -> CAP26EntityKind { Self::Address::entity_kind() } @@ -611,45 +968,32 @@ pub trait IsEntity: Into + Clone { fn e5() -> Self; fn e6() -> Self; fn e7() -> Self; - - fn securified_mainnet( - index: HDPathComponent, - name: impl AsRef, - make_matrix: fn(HDPathComponent) -> MatrixOfFactorInstances, - ) -> Self { - Self::new(name, make_matrix(index)) - } - - fn unsecurified_mainnet( - index: u32, - name: impl AsRef, - factor_source_id: FactorSourceIDFromHash, - ) -> Self { - Self::new( - name, - EntitySecurityState::Unsecured(HierarchicalDeterministicFactorInstance::mainnet_tx( - Self::kind(), - HDPathComponent::non_hardened(index), - factor_source_id, - )), - ) - } } #[derive(Clone, PartialEq, Eq, std::hash::Hash, derive_more::Debug)] #[debug("{}", self.address())] pub struct AbstractEntity + EntityKindSpecifier> { address: A, + pub name: String, pub security_state: EntitySecurityState, } pub type Account = AbstractEntity; + impl IsEntity for Account { - fn new(name: impl AsRef, security_state: impl Into) -> Self { + fn new( + name: impl AsRef, + address: Self::Address, + security_state: impl Into, + ) -> Self { Self { - address: AccountAddress::from(name.as_ref().to_owned()), + name: name.as_ref().to_owned(), + address, security_state: security_state.into(), } } + fn name(&self) -> String { + self.name.clone() + } type Address = AccountAddress; fn security_state(&self) -> EntitySecurityState { self.security_state.clone() @@ -685,9 +1029,14 @@ impl IsEntity for Account { pub type Persona = AbstractEntity; impl IsEntity for Persona { - fn new(name: impl AsRef, security_state: impl Into) -> Self { + fn new( + name: impl AsRef, + address: IdentityAddress, + security_state: impl Into, + ) -> Self { Self { - address: IdentityAddress::from(name.as_ref().to_owned()), + name: name.as_ref().to_owned(), + address, security_state: security_state.into(), } } @@ -695,6 +1044,9 @@ impl IsEntity for Persona { fn security_state(&self) -> EntitySecurityState { self.security_state.clone() } + fn name(&self) -> String { + self.name.clone() + } fn entity_address(&self) -> Self::Address { self.address.clone() } @@ -744,6 +1096,28 @@ impl From for AccountOrPersona { } } +impl TryFrom for Account { + type Error = CommonError; + + fn try_from(value: AccountOrPersona) -> Result { + match value { + AccountOrPersona::AccountEntity(a) => Ok(a), + AccountOrPersona::PersonaEntity(_) => Err(CommonError::Failure), + } + } +} + +impl TryFrom for Persona { + type Error = CommonError; + + fn try_from(value: AccountOrPersona) -> Result { + match value { + AccountOrPersona::PersonaEntity(p) => Ok(p), + AccountOrPersona::AccountEntity(_) => Err(CommonError::Failure), + } + } +} + impl From for AccountOrPersona { fn from(value: Persona) -> Self { Self::PersonaEntity(value) @@ -780,51 +1154,36 @@ impl HasSampleValues for Persona { } } -impl + EntityKindSpecifier + From> - AbstractEntity +impl< + T: IsEntityAddress + + Clone + + Into + + HasSampleValues + + EntityKindSpecifier, + > AbstractEntity +where + Self: IsEntity, { /// mainnet pub(crate) fn sample_unsecurified() -> Self { - Self::unsecurified_mainnet(0, "Alice", FactorSourceIDFromHash::fs0()) + ::unsecurified_mainnet( + "Sample Unsec", + HierarchicalDeterministicFactorInstance::fi0(T::entity_kind()), + ) } /// mainnet pub(crate) fn sample_securified() -> Self { - Self::securified_mainnet(6, "Grace", |idx| { - MatrixOfFactorInstances::m6(HierarchicalDeterministicFactorInstance::f( - Self::entity_kind(), - idx, - )) - }) - } - - fn new(name: impl AsRef, security_state: impl Into) -> Self { - Self { - address: T::from(name.as_ref().to_owned()), - security_state: security_state.into(), - } - } - - pub fn securified_mainnet( - index: u32, - name: impl AsRef, - make_matrix: impl Fn(HDPathComponent) -> MatrixOfFactorInstances, - ) -> Self { - Self::new(name, make_matrix(HDPathComponent::securified(index))) - } - - pub fn unsecurified_mainnet( - index: u32, - name: impl AsRef, - factor_source_id: FactorSourceIDFromHash, - ) -> Self { - Self::new( - name, - EntitySecurityState::Unsecured(HierarchicalDeterministicFactorInstance::mainnet_tx( - Self::entity_kind(), - HDPathComponent::non_hardened(index), - factor_source_id, - )), + ::securified_mainnet( + "Grace", + as IsEntity>::Address::sample_other(), + || { + let idx = HDPathComponent::securified(6); + MatrixOfFactorInstances::m6(HierarchicalDeterministicFactorInstance::f( + Self::entity_kind(), + idx, + )) + }, ) } } @@ -882,12 +1241,52 @@ where Self::new(factors, threshold, []) } + pub fn all_factors(&self) -> IndexSet { + let mut set = IndexSet::new(); + set.extend(self.threshold_factors.clone()); + set.extend(self.override_factors.clone()); + set + } + pub fn single_threshold(factor: F) -> Self { Self::threshold_only([factor], 1) } } pub type MatrixOfFactorInstances = MatrixOfFactors; + +impl MatrixOfFactorInstances { + pub fn fulfilling_matrix_of_factor_sources_with_instances( + instances: impl IntoIterator, + matrix_of_factor_sources: MatrixOfFactorSources, + ) -> Result { + let instances = instances.into_iter().collect_vec(); + + let get_factors = + |required: Vec| -> Result> { + required + .iter() + .map(|f| { + instances + .iter() + .find(|i| i.factor_source_id() == f.factor_source_id()) + .cloned() + .ok_or(CommonError::Failure) + }) + .collect::>>() + }; + + let threshold_factors = get_factors(matrix_of_factor_sources.threshold_factors)?; + let override_factors = get_factors(matrix_of_factor_sources.override_factors)?; + + Ok(Self::new( + threshold_factors, + matrix_of_factor_sources.threshold, + override_factors, + )) + } +} + pub type MatrixOfFactorSources = MatrixOfFactors; /// For unsecurified entities we map single factor -> single threshold factor. @@ -1022,6 +1421,7 @@ impl ManifestSummary { } } +#[derive(Clone, PartialEq, Eq, Debug)] pub struct Profile { pub factor_sources: IndexSet, pub accounts: HashMap, @@ -1029,11 +1429,33 @@ pub struct Profile { } impl Profile { + pub fn get_entities(&self) -> IndexSet { + match E::kind() { + CAP26EntityKind::Account => self + .accounts + .values() + .cloned() + .map(AccountOrPersona::from) + .map(|e| E::try_from(e).ok().unwrap()) + .collect::>(), + CAP26EntityKind::Identity => self + .personas + .values() + .cloned() + .map(AccountOrPersona::from) + .map(|e| E::try_from(e).ok().unwrap()) + .collect::>(), + } + } + pub fn get_accounts(&self) -> IndexSet { + self.get_entities() + } pub fn new<'a, 'p>( - factor_sources: IndexSet, + factor_sources: impl IntoIterator, accounts: impl IntoIterator, personas: impl IntoIterator, ) -> Self { + let factor_sources = factor_sources.into_iter().collect::>(); Self { factor_sources, accounts: accounts @@ -1052,18 +1474,47 @@ impl Profile { .ok_or(CommonError::UnknownAccount) .cloned() } + pub fn update_account(&mut self, account: Account) { + assert!(self + .accounts + .insert(account.entity_address(), account) + .is_some()); + } } #[derive(Clone, Debug, PartialEq, Eq, std::hash::Hash)] -pub struct Signature(String); +pub struct Signature([u8; 64]); +impl Signature { + pub fn new_with_hex(s: impl AsRef) -> Result { + hex::decode(s.as_ref()) + .map_err(|_| CommonError::Failure) + .and_then(|b| b.try_into().map_err(|_| CommonError::Failure)) + .map(Self) + } +} impl HasSampleValues for Signature { fn sample() -> Self { - Self("deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef".to_owned()) + Self::new_with_hex("deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef").unwrap() } fn sample_other() -> Self { - Self("fadecafefadecafefadecafefadecafefadecafefadecafefadecafefadecafefadecafefadecafefadecafefadecafefadecafefadecafefadecafefadecafe".to_owned()) + Self::new_with_hex("fadecafefadecafefadecafefadecafefadecafefadecafefadecafefadecafefadecafefadecafefadecafefadecafefadecafefadecafefadecafefadecafe").unwrap() + } +} + +#[cfg(test)] +mod signature_tests { + use super::*; + + type Sut = Signature; + + #[test] + fn eq() { + assert_eq!(Sut::sample(), Sut::sample()); + assert_eq!(Sut::sample_other(), Sut::sample_other()); + assert_ne!(Sut::sample(), Sut::sample_other()); } } + impl Signature { /// Emulates the signing of `intent_hash` with `factor_instance` - in a /// deterministic manner. @@ -1076,8 +1527,9 @@ impl Signature { let intent_hash_bytes = intent_hash.hash().to_bytes(); let factor_instance_bytes = factor_instance.to_bytes(); let input_bytes = [intent_hash_bytes, factor_instance_bytes].concat(); - let hash = sha256::digest(input_bytes); - Self(hash) + let mut hasher = sha2::Sha512::new(); + hasher.update(input_bytes); + Self(hasher.finalize().into()) } /// Emulates signing using `input`. @@ -1111,3 +1563,254 @@ pub enum CommonError { #[error("Unknown persona")] UnknownPersona, } + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct AccessControllerAddress(pub String); +impl AccessControllerAddress { + pub fn new(a: A) -> Self { + Self(format!( + "access_controller_{:?}_{:?}", + a.network_id(), + a.public_key_hash() + )) + } + // pub fn generate() -> Self { + // Self::new(Uuid::new_v4().to_string()) + // } +} + +#[derive(Clone, PartialEq, Eq, Hash, derive_more::Debug)] +#[debug("{}", hex::encode(&self.0[28..32]))] +pub struct PublicKeyHash([u8; 32]); + +impl PublicKeyHash { + pub fn new(bytes: [u8; 32]) -> Self { + Self(bytes) + } + pub fn repeat(byte: u8) -> Self { + Self::new([byte; 32]) + } + pub fn sample_0() -> Self { + Self::repeat(0x50) + } + pub fn sample_1() -> Self { + Self::repeat(0x51) + } + pub fn sample_2() -> Self { + Self::repeat(0x52) + } + pub fn sample_3() -> Self { + Self::repeat(0x53) + } + pub fn sample_4() -> Self { + Self::repeat(0x54) + } + pub fn sample_5() -> Self { + Self::repeat(0x55) + } + pub fn sample_6() -> Self { + Self::repeat(0x56) + } + pub fn sample_7() -> Self { + Self::repeat(0x57) + } + pub fn sample_8() -> Self { + Self::repeat(0x58) + } + pub fn sample_9() -> Self { + Self::repeat(0x59) + } +} +impl HasSampleValues for PublicKeyHash { + fn sample() -> Self { + Self::sample_0() + } + fn sample_other() -> Self { + Self::sample_1() + } +} + +impl PublicKey { + pub fn hash(&self) -> PublicKeyHash { + let mut hasher = Sha256::new(); + hasher.update(self.to_bytes()); + let digest = hasher.finalize().into(); + PublicKeyHash(digest) + } +} + +impl HierarchicalDeterministicPublicKey { + pub fn hash(&self) -> PublicKeyHash { + self.public_key.hash() + } +} +impl HierarchicalDeterministicFactorInstance { + pub fn public_key_hash(&self) -> PublicKeyHash { + self.public_key.hash() + } +} +impl From for PublicKeyHash { + fn from(value: PublicKey) -> Self { + value.hash() + } +} +impl From for PublicKeyHash { + fn from(value: HierarchicalDeterministicPublicKey) -> Self { + value.hash() + } +} +impl From for PublicKeyHash { + fn from(value: HierarchicalDeterministicFactorInstance) -> Self { + value.public_key_hash() + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash, EnumAsInner)] +pub enum ScryptoResourceOrNonFungible { + PublicKeyHash(PublicKeyHash), +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash, EnumAsInner)] +pub enum ScryptoProofRule { + AnyOf(Vec), + CountOf(usize, Vec), + // AllOf + // Require + // AmountOf +} +impl ScryptoProofRule { + pub fn any_of(values: Vec) -> Self { + Self::AnyOf(values) + } + pub fn count_of(count: usize, values: Vec) -> Self { + Self::CountOf(count, values) + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash, EnumAsInner)] +pub enum ScryptoAccessRuleNode { + ProofRule(ScryptoProofRule), + AnyOf(Vec), + AllOf(Vec), +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash, EnumAsInner)] +pub enum ScryptoAccessRule { + Protected(ScryptoAccessRuleNode), + // AllowAll + // DenyAll +} +impl ScryptoAccessRule { + pub fn protected(rule: ScryptoAccessRuleNode) -> Self { + Self::Protected(rule) + } + pub fn with_threshold( + count: usize, + threshold_factors: impl IntoIterator>, + override_factors: impl IntoIterator>, + ) -> Self { + Self::protected(ScryptoAccessRuleNode::AnyOf(vec![ + ScryptoAccessRuleNode::ProofRule(ScryptoProofRule::CountOf( + count, + threshold_factors + .into_iter() + .map(Into::into) + .map(ScryptoResourceOrNonFungible::PublicKeyHash) + .collect_vec(), + )), + ScryptoAccessRuleNode::ProofRule(ScryptoProofRule::AnyOf( + override_factors + .into_iter() + .map(Into::into) + .map(ScryptoResourceOrNonFungible::PublicKeyHash) + .collect_vec(), + )), + ])) + } +} + +pub type MatrixOfKeyHashes = MatrixOfFactors; +impl From for ScryptoAccessRule { + fn from(value: MatrixOfFactorInstances) -> Self { + Self::with_threshold( + value.threshold as usize, + value.threshold_factors, + value.override_factors, + ) + } +} +impl From for ScryptoAccessRule { + fn from(value: MatrixOfKeyHashes) -> Self { + Self::with_threshold( + value.threshold as usize, + value.threshold_factors, + value.override_factors, + ) + } +} +impl TryFrom for MatrixOfKeyHashes { + type Error = CommonError; + + fn try_from(value: ScryptoAccessRule) -> Result { + let protected = value.into_protected().map_err(|_| CommonError::Failure)?; + let root_any_of = protected.into_any_of().map_err(|_| CommonError::Failure)?; + if root_any_of.len() != 2 { + return Err(CommonError::Failure); + } + let rule_0 = root_any_of[0] + .clone() + .into_proof_rule() + .map_err(|_| CommonError::Failure)?; + + let rule_1 = root_any_of[1] + .clone() + .into_proof_rule() + .map_err(|_| CommonError::Failure)?; + + let threshold_rule = rule_0.into_count_of().map_err(|_| CommonError::Failure)?; + let override_rule = rule_1.into_any_of().map_err(|_| CommonError::Failure)?; + + let threshold = threshold_rule.0; + let threshold_hashes = threshold_rule + .1 + .into_iter() + .map(|r| r.into_public_key_hash().map_err(|_| CommonError::Failure)) + .collect::>>()?; + + let override_hashes = override_rule + .into_iter() + .map(|r| r.into_public_key_hash().map_err(|_| CommonError::Failure)) + .collect::>>()?; + + Ok(Self::new( + threshold_hashes, + threshold as u8, + override_hashes, + )) + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct ComponentMetadata { + pub scrypto_access_rules: ScryptoAccessRule, +} + +impl ComponentMetadata { + pub fn new(scrypto_access_rules: impl Into) -> Self { + Self { + scrypto_access_rules: scrypto_access_rules.into(), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct AccessController { + pub address: AccessControllerAddress, + pub metadata: ComponentMetadata, +} + +impl AccessController { + pub fn new(address: AccessControllerAddress, metadata: ComponentMetadata) -> Self { + Self { address, metadata } + } +} diff --git a/tests/main.rs b/tests/main.rs index 2cf64388..d63c59b9 100644 --- a/tests/main.rs +++ b/tests/main.rs @@ -37,7 +37,7 @@ mod integration_test_derivation { request: MonoFactorKeyDerivationRequest, ) -> Result { let factor_source_id = request.clone().factor_source_id; - Ok(KeyDerivationResponse::new(IndexMap::from_iter([( + Ok(KeyDerivationResponse::new(IndexMap::just(( factor_source_id, request .derivation_paths @@ -47,7 +47,7 @@ mod integration_test_derivation { HierarchicalDeterministicFactorInstance::mocked_with(p, &factor_source_id) }) .collect(), - )]))) + )))) } } @@ -77,32 +77,32 @@ mod integration_test_derivation { DerivationPath::account_tx(NetworkID::Mainnet, HDPathComponent::securified(1)), DerivationPath::account_tx( NetworkID::Stokenet, - HDPathComponent::non_hardened(2), + HDPathComponent::unsecurified(2), ), ]), ), ( f1.factor_source_id(), - IndexSet::<_>::from_iter([DerivationPath::account_tx( + IndexSet::<_>::just(DerivationPath::account_tx( NetworkID::Stokenet, - HDPathComponent::non_hardened(3), - )]), + HDPathComponent::unsecurified(3), + )), ), ( f2.factor_source_id(), - IndexSet::<_>::from_iter([DerivationPath::account_tx( + IndexSet::<_>::just(DerivationPath::account_tx( NetworkID::Mainnet, - HDPathComponent::non_hardened(4), - )]), + HDPathComponent::unsecurified(4), + )), ), ( f3.factor_source_id(), - IndexSet::<_>::from_iter([DerivationPath::new( + IndexSet::<_>::just(DerivationPath::new( NetworkID::Mainnet, CAP26EntityKind::Identity, CAP26KeyKind::Rola, HDPathComponent::securified(5), - )]), + )), ), ]); @@ -182,7 +182,7 @@ mod integration_test_signing { if request.invalid_transactions_if_neglected.is_empty() { return SignWithFactorsOutcome::Neglected(NeglectedFactors::new( NeglectFactorReason::UserExplicitlySkipped, - IndexSet::from_iter([request.input.factor_source_id]), + IndexSet::just(request.input.factor_source_id), )); } let signatures = request @@ -223,7 +223,8 @@ mod integration_test_signing { let f3 = HDFactorSource::arculus(); let f4 = HDFactorSource::off_device(); - let alice = Account::securified_mainnet(0, "Alice", |i| { + let alice = Account::securified_mainnet("Alice", AccountAddress::sample(), || { + let i = HDPathComponent::securified(0); MatrixOfFactorInstances::threshold_only( [ FI::mainnet_tx_account(i, f0.factor_source_id()), // SKIPPED @@ -234,14 +235,16 @@ mod integration_test_signing { ) }); - let bob = Account::securified_mainnet(1, "Bob", |i| { + let bob = Account::securified_mainnet("Bob", AccountAddress::sample_2(), || { + let i = HDPathComponent::securified(1); MatrixOfFactorInstances::override_only([FI::mainnet_tx_account( i, f3.factor_source_id(), )]) }); - let carol = Account::securified_mainnet(2, "Carol", |i| { + let carol = Account::securified_mainnet("Carol", AccountAddress::sample_3(), || { + let i = HDPathComponent::securified(2); MatrixOfFactorInstances::new( [FI::mainnet_tx_account(i, f2.factor_source_id())], 1, @@ -249,7 +252,13 @@ mod integration_test_signing { ) }); - let satoshi = Persona::unsecurified_mainnet(1337, "Satoshi", f4.factor_source_id()); + let satoshi = Persona::unsecurified_mainnet( + "Satoshi", + HierarchicalDeterministicFactorInstance::mainnet_tx_identity( + HDPathComponent::unsecurified(0), + f4.factor_source_id(), + ), + ); let tx0 = TransactionIntent::new([alice.entity_address()], []); let tx1 = TransactionIntent::new( @@ -265,7 +274,7 @@ mod integration_test_signing { let transactions = [tx0, tx1, tx2]; let profile = Profile::new( - IndexSet::from_iter([f0.clone(), f1, f2, f3, f4]), + [f0.clone(), f1, f2, f3, f4], [&alice, &bob, &carol], [&satoshi], ); @@ -284,7 +293,7 @@ mod integration_test_signing { assert_eq!(outcome.signatures_of_successful_transactions().len(), 10); assert_eq!( outcome.ids_of_neglected_factor_sources(), - IndexSet::::from_iter([f0.factor_source_id()]) + IndexSet::::just(f0.factor_source_id()) ); } }