diff --git a/src/frontend/src/lib/generated/internet_identity_idl.js b/src/frontend/src/lib/generated/internet_identity_idl.js index 4ab557ba8c..c1d6a4a080 100644 --- a/src/frontend/src/lib/generated/internet_identity_idl.js +++ b/src/frontend/src/lib/generated/internet_identity_idl.js @@ -178,7 +178,7 @@ export const idlFactory = ({ IDL }) => { }); const FrontendHostname = IDL.Text; const AccountNumber = IDL.Nat64; - const Account = IDL.Record({ + const AccountInfo = IDL.Record({ 'name' : IDL.Opt(IDL.Text), 'origin' : IDL.Text, 'account_number' : IDL.Opt(AccountNumber), @@ -519,7 +519,7 @@ export const idlFactory = ({ IDL }) => { 'config' : IDL.Func([], [InternetIdentityInit], ['query']), 'create_account' : IDL.Func( [UserNumber, FrontendHostname, IDL.Text], - [IDL.Variant({ 'Ok' : Account, 'Err' : CreateAccountError })], + [IDL.Variant({ 'Ok' : AccountInfo, 'Err' : CreateAccountError })], [], ), 'create_challenge' : IDL.Func([], [Challenge], []), @@ -534,7 +534,7 @@ export const idlFactory = ({ IDL }) => { ), 'get_accounts' : IDL.Func( [UserNumber, FrontendHostname], - [IDL.Vec(Account)], + [IDL.Vec(AccountInfo)], ['query'], ), 'get_anchor_credentials' : IDL.Func( @@ -664,7 +664,7 @@ export const idlFactory = ({ IDL }) => { 'update' : IDL.Func([UserNumber, DeviceKey, DeviceData], [], []), 'update_account' : IDL.Func( [UserNumber, FrontendHostname, IDL.Opt(AccountNumber), AccountUpdate], - [IDL.Variant({ 'Ok' : Account, 'Err' : UpdateAccountError })], + [IDL.Variant({ 'Ok' : AccountInfo, 'Err' : UpdateAccountError })], [], ), 'verify_tentative_device' : IDL.Func( diff --git a/src/frontend/src/lib/generated/internet_identity_types.d.ts b/src/frontend/src/lib/generated/internet_identity_types.d.ts index c72a7f7fe2..a592678503 100644 --- a/src/frontend/src/lib/generated/internet_identity_types.d.ts +++ b/src/frontend/src/lib/generated/internet_identity_types.d.ts @@ -2,7 +2,7 @@ import type { Principal } from '@dfinity/principal'; import type { ActorMethod } from '@dfinity/agent'; import type { IDL } from '@dfinity/candid'; -export interface Account { +export interface AccountInfo { 'name' : [] | [string], 'origin' : string, 'account_number' : [] | [AccountNumber], @@ -423,7 +423,7 @@ export interface _SERVICE { 'config' : ActorMethod<[], InternetIdentityInit>, 'create_account' : ActorMethod< [UserNumber, FrontendHostname, string], - { 'Ok' : Account } | + { 'Ok' : AccountInfo } | { 'Err' : CreateAccountError } >, 'create_challenge' : ActorMethod<[], Challenge>, @@ -435,7 +435,10 @@ export interface _SERVICE { [UserNumber, FrontendHostname, AccountNumber, SessionKey, Timestamp], GetDelegationResponse >, - 'get_accounts' : ActorMethod<[UserNumber, FrontendHostname], Array>, + 'get_accounts' : ActorMethod< + [UserNumber, FrontendHostname], + Array + >, 'get_anchor_credentials' : ActorMethod<[UserNumber], AnchorCredentials>, 'get_anchor_info' : ActorMethod<[UserNumber], IdentityAnchorInfo>, 'get_delegation' : ActorMethod< @@ -535,7 +538,7 @@ export interface _SERVICE { 'update' : ActorMethod<[UserNumber, DeviceKey, DeviceData], undefined>, 'update_account' : ActorMethod< [UserNumber, FrontendHostname, [] | [AccountNumber], AccountUpdate], - { 'Ok' : Account } | + { 'Ok' : AccountInfo } | { 'Err' : UpdateAccountError } >, 'verify_tentative_device' : ActorMethod< diff --git a/src/internet_identity/internet_identity.did b/src/internet_identity/internet_identity.did index 5a23170b96..3a3d994c4f 100644 --- a/src/internet_identity/internet_identity.did +++ b/src/internet_identity/internet_identity.did @@ -690,7 +690,7 @@ type IdRegFinishError = variant { StorageError : text; }; -type Account = record { +type AccountInfo = record { // Null is unreserved default account account_number : opt AccountNumber; origin : text; @@ -843,20 +843,20 @@ service : (opt InternetIdentityInit) -> { get_accounts : ( anchor_number : UserNumber, origin : FrontendHostname, - ) -> (vec Account) query; + ) -> (vec AccountInfo) query; create_account : ( anchor_number : UserNumber, origin : FrontendHostname, name : text - ) -> (variant { Ok : Account; Err: CreateAccountError }); + ) -> (variant { Ok : AccountInfo; Err: CreateAccountError }); update_account : ( anchor_number : UserNumber, origin : FrontendHostname, account_number : opt AccountNumber, // Null is unreserved default account update : AccountUpdate - ) -> (variant { Ok : Account; Err: UpdateAccountError }); + ) -> (variant { Ok : AccountInfo; Err: UpdateAccountError }); prepare_account_delegation : ( anchor_number : UserNumber, diff --git a/src/internet_identity/src/main.rs b/src/internet_identity/src/main.rs index bf0e5751c6..dc0d51a17a 100644 --- a/src/internet_identity/src/main.rs +++ b/src/internet_identity/src/main.rs @@ -298,15 +298,15 @@ fn get_delegation( } #[query] -fn get_accounts(_anchor_number: AnchorNumber, _origin: FrontendHostname) -> Vec { +fn get_accounts(_anchor_number: AnchorNumber, _origin: FrontendHostname) -> Vec { vec![ - Account { + AccountInfo { account_number: None, origin: "example.com".to_string(), last_used: Some(0u64), name: Some("Default Mock Account".to_string()), }, - Account { + AccountInfo { account_number: Some(1), origin: "example.com".to_string(), last_used: Some(0u64), @@ -320,8 +320,8 @@ fn create_account( _anchor_number: AnchorNumber, _origin: FrontendHostname, name: String, -) -> Result { - Ok(Account { +) -> Result { + Ok(AccountInfo { account_number: Some(ic_cdk::api::time()), origin: "example.com".to_string(), last_used: None, @@ -335,8 +335,8 @@ fn update_account( _origin: FrontendHostname, _account_number: Option, _update: AccountUpdate, -) -> Result { - Ok(Account { +) -> Result { + Ok(AccountInfo { account_number: None, origin: "example.com".to_string(), last_used: Some(0u64), diff --git a/src/internet_identity/src/storage.rs b/src/internet_identity/src/storage.rs index 4a01a3709c..e9f110f7f8 100644 --- a/src/internet_identity/src/storage.rs +++ b/src/internet_identity/src/storage.rs @@ -79,6 +79,10 @@ //! //! The archive buffer memory is managed by the [MemoryManager] and is currently limited to a single //! bucket of 128 pages. +use account::{ + Account, AccountType, AccountsCounter, CreateAccountParams, ReadAccountParams, + UpdateAccountParams, UpdateExistinAccountParams, +}; use candid::{CandidType, Deserialize}; use ic_cdk::api::stable::WASM_PAGE_SIZE_IN_BYTES; use std::borrow::Cow; @@ -86,6 +90,8 @@ use std::collections::{BTreeSet, HashMap}; use std::fmt; use std::io::{Read, Write}; use std::ops::RangeInclusive; +use storable_account_reference_list::StorableAccountReferenceList; +use storable_anchor_number_list::StorableAnchorNumberList; use ic_cdk::api::trap; use ic_stable_structures::memory_manager::{MemoryId, MemoryManager, VirtualMemory}; @@ -101,12 +107,13 @@ use crate::openid::{OpenIdCredential, OpenIdCredentialKey}; use crate::state::PersistentState; use crate::stats::event_stats::AggregationKey; use crate::stats::event_stats::{EventData, EventKey}; +use crate::storage::account::{AccountReference, StorableAccount}; use crate::storage::anchor::{Anchor, Device}; +use crate::storage::application::{Application, OriginHash}; use crate::storage::memory_wrapper::MemoryWrapper; use crate::storage::registration_rates::RegistrationRates; use crate::storage::stable_anchor::StableAnchor; use crate::storage::storable_anchor::StorableAnchor; -use crate::storage::storable_anchor_number_list::StorableAnchorNumberList; use crate::storage::storable_credential_id::StorableCredentialId; use crate::storage::storable_openid_credential_key::StorableOpenIdCredentialKey; use crate::storage::storable_persistent_state::StorablePersistentState; @@ -115,8 +122,12 @@ use internet_identity_interface::internet_identity::types::*; pub mod anchor; pub mod registration_rates; +pub mod account; +pub mod application; + pub mod stable_anchor; /// module for the internal serialization format of anchors +mod storable_account_reference_list; mod storable_anchor; mod storable_anchor_number_list; mod storable_credential_id; @@ -146,6 +157,12 @@ const REGISTRATION_CURRENT_RATE_MEMORY_INDEX: u8 = 6u8; const STABLE_ANCHOR_MEMORY_INDEX: u8 = 7u8; const LOOKUP_ANCHOR_WITH_OPENID_CREDENTIAL_MEMORY_INDEX: u8 = 8u8; const LOOKUP_ANCHOR_WITH_DEVICE_CREDENTIAL_MEMORY_INDEX: u8 = 9u8; +const STABLE_ACCOUNT_MEMORY_INDEX: u8 = 10u8; +const STABLE_APPLICATION_MEMORY_INDEX: u8 = 11u8; +const LOOKUP_APPLICATION_WITH_ORIGIN_MEMORY_INDEX: u8 = 12u8; +const STABLE_ACCOUNT_REFERENCE_LIST_MEMORY_INDEX: u8 = 13u8; +const STABLE_ANCHOR_ACCOUNT_COUNTER_MEMORY_INDEX: u8 = 14u8; +const STABLE_ACCOUNT_COUNTER_MEMORY_INDEX: u8 = 15u8; const ANCHOR_MEMORY_ID: MemoryId = MemoryId::new(ANCHOR_MEMORY_INDEX); const ARCHIVE_BUFFER_MEMORY_ID: MemoryId = MemoryId::new(ARCHIVE_BUFFER_MEMORY_INDEX); const PERSISTENT_STATE_MEMORY_ID: MemoryId = MemoryId::new(PERSISTENT_STATE_MEMORY_INDEX); @@ -156,10 +173,20 @@ const REGISTRATION_REFERENCE_RATE_MEMORY_ID: MemoryId = const REGISTRATION_CURRENT_RATE_MEMORY_ID: MemoryId = MemoryId::new(REGISTRATION_CURRENT_RATE_MEMORY_INDEX); const STABLE_ANCHOR_MEMORY_ID: MemoryId = MemoryId::new(STABLE_ANCHOR_MEMORY_INDEX); +const STABLE_ACCOUNT_MEMORY_ID: MemoryId = MemoryId::new(STABLE_ACCOUNT_MEMORY_INDEX); +const STABLE_APPLICATION_MEMORY_ID: MemoryId = MemoryId::new(STABLE_APPLICATION_MEMORY_INDEX); +const STABLE_ACCOUNT_REFERENCE_LIST_MEMORY_ID: MemoryId = + MemoryId::new(STABLE_ACCOUNT_REFERENCE_LIST_MEMORY_INDEX); +const STABLE_ACCOUNT_COUNTER_MEMORY_ID: MemoryId = + MemoryId::new(STABLE_ACCOUNT_COUNTER_MEMORY_INDEX); +const STABLE_ANCHOR_ACCOUNT_COUNTER_MEMORY_ID: MemoryId = + MemoryId::new(STABLE_ANCHOR_ACCOUNT_COUNTER_MEMORY_INDEX); const LOOKUP_ANCHOR_WITH_OPENID_CREDENTIAL_MEMORY_ID: MemoryId = MemoryId::new(LOOKUP_ANCHOR_WITH_OPENID_CREDENTIAL_MEMORY_INDEX); const LOOKUP_ANCHOR_WITH_DEVICE_CREDENTIAL_MEMORY_ID: MemoryId = MemoryId::new(LOOKUP_ANCHOR_WITH_DEVICE_CREDENTIAL_MEMORY_INDEX); +const LOOKUP_APPLICATION_WITH_ORIGIN_MEMORY_ID: MemoryId = + MemoryId::new(LOOKUP_APPLICATION_WITH_ORIGIN_MEMORY_INDEX); // The bucket size 128 is relatively low, to avoid wasting memory when using // multiple virtual memories for smaller amounts of data. // This value results in 256 GB of total managed memory, which should be enough @@ -222,6 +249,24 @@ pub struct Storage { /// Memory wrapper used to report the size of the stable anchor memory. stable_anchor_memory_wrapper: MemoryWrapper>, stable_anchor_memory: StableBTreeMap>, + /// Memory wrapper used to report the size of the stable account memory. + stable_account_memory_wrapper: MemoryWrapper>, + stable_account_memory: StableBTreeMap>, + /// Memory wrapper used to report the size of the stable application memory. + stable_application_memory_wrapper: MemoryWrapper>, + stable_application_memory: StableBTreeMap>, + /// Memory wrapper used to report the size of the stable account counter memory. + stable_anchor_account_counter_memory_wrapper: MemoryWrapper>, + stable_anchor_account_counter_memory: + StableBTreeMap>, + /// Memory wrapper used to report the size of the stable account reference list memory. + stable_account_reference_list_memory_wrapper: MemoryWrapper>, + stable_account_reference_list_memory: StableBTreeMap< + (AccountNumber, ApplicationNumber), + StorableAccountReferenceList, + ManagedMemory, + >, + stable_account_counter_memory: StableCell>, /// Memory wrapper used to report the size of the lookup anchor with OpenID credential memory. lookup_anchor_with_openid_credential_memory_wrapper: MemoryWrapper>, lookup_anchor_with_openid_credential_memory: @@ -230,6 +275,9 @@ pub struct Storage { lookup_anchor_with_device_credential_memory_wrapper: MemoryWrapper>, lookup_anchor_with_device_credential_memory: StableBTreeMap>, + lookup_application_with_origin_memory_wrapper: MemoryWrapper>, + lookup_application_with_origin_memory: + StableBTreeMap>, } #[repr(C, packed)] @@ -292,10 +340,19 @@ impl Storage { let registration_current_rate_memory = memory_manager.get(REGISTRATION_CURRENT_RATE_MEMORY_ID); let stable_anchor_memory = memory_manager.get(STABLE_ANCHOR_MEMORY_ID); + let stable_account_memory = memory_manager.get(STABLE_ACCOUNT_MEMORY_ID); + let stable_application_memory = memory_manager.get(STABLE_APPLICATION_MEMORY_ID); + let stable_anchor_account_counter_memory = + memory_manager.get(STABLE_ANCHOR_ACCOUNT_COUNTER_MEMORY_ID); + let stable_account_reference_list_memory = + memory_manager.get(STABLE_ACCOUNT_REFERENCE_LIST_MEMORY_ID); + let stable_account_counter_memory = memory_manager.get(STABLE_ACCOUNT_COUNTER_MEMORY_ID); let lookup_anchor_with_openid_credential_memory = memory_manager.get(LOOKUP_ANCHOR_WITH_OPENID_CREDENTIAL_MEMORY_ID); let lookup_anchor_with_device_credential_memory = memory_manager.get(LOOKUP_ANCHOR_WITH_DEVICE_CREDENTIAL_MEMORY_ID); + let lookup_application_with_origin_memory = + memory_manager.get(LOOKUP_APPLICATION_WITH_ORIGIN_MEMORY_ID); let registration_rates = RegistrationRates::new( MinHeap::init(registration_ref_rate_memory.clone()) @@ -330,6 +387,29 @@ impl Storage { event_aggregations: StableBTreeMap::init(stats_aggregations_memory), stable_anchor_memory_wrapper: MemoryWrapper::new(stable_anchor_memory.clone()), stable_anchor_memory: StableBTreeMap::init(stable_anchor_memory), + stable_account_memory_wrapper: MemoryWrapper::new(stable_account_memory.clone()), + stable_account_memory: StableBTreeMap::init(stable_account_memory), + stable_application_memory_wrapper: MemoryWrapper::new( + stable_application_memory.clone(), + ), + stable_application_memory: StableBTreeMap::init(stable_application_memory), + stable_anchor_account_counter_memory_wrapper: MemoryWrapper::new( + stable_anchor_account_counter_memory.clone(), + ), + stable_anchor_account_counter_memory: StableBTreeMap::init( + stable_anchor_account_counter_memory, + ), + stable_account_reference_list_memory_wrapper: MemoryWrapper::new( + stable_account_reference_list_memory.clone(), + ), + stable_account_reference_list_memory: StableBTreeMap::init( + stable_account_reference_list_memory, + ), + stable_account_counter_memory: StableCell::init( + stable_account_counter_memory, + AccountsCounter::default(), + ) + .expect("stable_account_counter_memory"), lookup_anchor_with_openid_credential_memory_wrapper: MemoryWrapper::new( lookup_anchor_with_openid_credential_memory.clone(), ), @@ -342,6 +422,12 @@ impl Storage { lookup_anchor_with_device_credential_memory: StableBTreeMap::init( lookup_anchor_with_device_credential_memory, ), + lookup_application_with_origin_memory_wrapper: MemoryWrapper::new( + lookup_application_with_origin_memory.clone(), + ), + lookup_application_with_origin_memory: StableBTreeMap::init( + lookup_application_with_origin_memory, + ), } } @@ -597,6 +683,400 @@ impl Storage { .get(&key.clone().into()) } + /// Look up an application number per origin, create entry in applications and lookup table if it doesn't exist + pub fn lookup_or_insert_application_number_with_origin( + &mut self, + origin: &FrontendHostname, + ) -> ApplicationNumber { + let origin_hash = OriginHash::from_origin(origin); + + if let Some(existing_number) = self.lookup_application_with_origin_memory.get(&origin_hash) + { + existing_number + } else { + let new_number: ApplicationNumber = self.lookup_application_with_origin_memory.len(); + + self.lookup_application_with_origin_memory + .insert(origin_hash, new_number); + + let new_application = Application { + origin: origin.to_string(), + stored_accounts: 0u64, + stored_account_references: 0u64, + }; + + self.stable_application_memory + .insert(new_number, new_application); + new_number + } + } + + pub fn lookup_application_number_with_origin( + &self, + origin: &FrontendHostname, + ) -> Option { + self.lookup_application_with_origin_memory + .get(&OriginHash::from_origin(origin)) + } + + #[allow(dead_code)] + pub fn lookup_application_with_origin(&self, origin: &FrontendHostname) -> Option { + self.lookup_application_number_with_origin(origin) + .and_then(|application_number| self.stable_application_memory.get(&application_number)) + } + + fn lookup_account_references( + &self, + anchor_number: AnchorNumber, + application_number: ApplicationNumber, + ) -> Option> { + self.stable_account_reference_list_memory + .get(&(anchor_number, application_number)) + .map(|list| list.into()) + } + + /// Updates the anchor account, application and account counters. + /// It doesn't update the account conter for Account type. + /// Because that one is updated when a new account number is allocated with `allocate_account_number`. + fn update_counters( + &mut self, + application_number: ApplicationNumber, + anchor_number: AnchorNumber, + account_type: AccountType, + ) -> Result<(), StorageError> { + let anchor_account_counter = self + .stable_anchor_account_counter_memory + .get(&anchor_number) + .unwrap_or(AccountsCounter { + stored_accounts: 0, + stored_account_references: 0, + }); + self.stable_anchor_account_counter_memory.insert( + anchor_number, + anchor_account_counter.increment(&account_type), + ); + + // The account counter is updated when a new account number is allocated with `allocate_account_number`. + if account_type == AccountType::AccountReference { + let account_number = self.stable_account_counter_memory.get(); + self.stable_account_counter_memory + .set(account_number.increment(&account_type)) + .map_err(|_| StorageError::ErrorUpdatingAccountCounter)?; + } + + if let Some(mut application) = self.stable_application_memory.get(&application_number) { + match account_type { + AccountType::Account => application.stored_accounts += 1, + AccountType::AccountReference => application.stored_account_references += 1, + } + self.stable_application_memory + .insert(application_number, application); + } + Ok(()) + } + + #[allow(dead_code)] + /// Returns the account counter for a given anchor number. + pub fn get_account_counter(&self, anchor_number: AnchorNumber) -> AccountsCounter { + self.stable_anchor_account_counter_memory + .get(&anchor_number) + .unwrap_or(AccountsCounter { + stored_accounts: 0, + stored_account_references: 0, + }) + } + + #[allow(dead_code)] + /// Returns the total account counter. + pub fn get_total_accounts_counter(&self) -> &AccountsCounter { + self.stable_account_counter_memory.get() + } + + // Increments the `stable_account_counter_memory` account counter by one and returns the new number. + fn allocate_account_number(&mut self) -> Result { + let account_conter = self.stable_account_counter_memory.get(); + let updated_accounts_counter = account_conter.increment(&AccountType::Account); + let next_account_number = updated_accounts_counter.stored_accounts; + self.stable_account_counter_memory + .set(updated_accounts_counter) + .map_err(|_| StorageError::ErrorUpdatingAccountCounter)?; + Ok(next_account_number) + } + + #[allow(dead_code)] + /// Returns all account references associated with a single anchor number, across all applications. + pub fn list_identity_accounts(&self, anchor_number: AnchorNumber) -> Vec { + let mut all_accounts: Vec = Vec::new(); + + let range_start = (anchor_number, ApplicationNumber::MIN); + let range_end = (anchor_number, ApplicationNumber::MAX); + + for ((_found_anchor, _app_num), storable_account_ref_list_val) in self + .stable_account_reference_list_memory + .range(range_start..=range_end) + { + let storable_refs_vec: Vec = storable_account_ref_list_val.into(); + + for storable_ref in storable_refs_vec { + all_accounts.push(AccountReference { + account_number: storable_ref.account_number, + last_used: storable_ref.last_used, + }); + } + } + + all_accounts + } + + #[allow(dead_code)] + pub fn create_additional_account( + &mut self, + params: CreateAccountParams, + ) -> Result { + let anchor_number = params.anchor_number; + let origin = ¶ms.origin; + + // Create and store account in stable memory + let account_number = self.allocate_account_number()?; + let storable_account = StorableAccount { + name: params.name.clone(), + seed_from_anchor: None, + }; + self.stable_account_memory + .insert(account_number, storable_account); + + // Update application data + let app_num = self.lookup_or_insert_application_number_with_origin(origin); + + // Update counters with one more account. + self.update_counters(app_num, anchor_number, AccountType::Account)?; + + // Process account references + match self + .stable_account_reference_list_memory + .get(&(anchor_number, app_num)) + { + None => { + // Two new account references were created. + self.update_counters(app_num, anchor_number, AccountType::AccountReference)?; + self.update_counters(app_num, anchor_number, AccountType::AccountReference)?; + // If no list exists for this anchor & application, + // Create and insert the default and additional account. + // This is because we don't create default accounts explicitly. + let additional_account_reference = AccountReference { + account_number: Some(account_number), + last_used: None, + }; + let default_account_reference = AccountReference { + account_number: None, + last_used: None, + }; + self.stable_account_reference_list_memory.insert( + (anchor_number, app_num), + vec![default_account_reference, additional_account_reference].into(), + ); + } + Some(existing_storable_list) => { + self.update_counters(app_num, anchor_number, AccountType::AccountReference)?; + // If the list exists, push the new account and reinsert it to memory + let mut refs_vec: Vec = existing_storable_list.into(); + refs_vec.push(AccountReference { + account_number: Some(account_number), + last_used: None, + }); + self.stable_account_reference_list_memory + .insert((anchor_number, app_num), refs_vec.into()); + } + } + + // Return the new account + Ok(Account { + account_number: Some(account_number), + anchor_number, + origin: origin.clone(), + last_used: None, + name: Some(params.name), + }) + } + + #[allow(dead_code)] + /// Returns a list of account references for a given anchor and application. + /// If the application doesn't exist, returns a list with a default account reference. + /// If the account references doesn't exist, returns a list with a default account reference. + pub fn list_accounts( + &self, + anchor_number: &AnchorNumber, + origin: &FrontendHostname, + ) -> Result, StorageError> { + match self.lookup_application_number_with_origin(origin) { + None => Ok(vec![AccountReference { + account_number: None, + last_used: None, + }]), + Some(app_num) => match self.lookup_account_references(*anchor_number, app_num) { + None => Ok(vec![AccountReference { + account_number: None, + last_used: None, + }]), + Some(refs) => Ok(refs), + }, + } + } + + #[allow(dead_code)] + /// Returns the requested account. + /// If the account number doesn't esist, returns a default Account. + /// If the account number exists but the account doesn't exist, returns None. + /// If the account exists, returns it as Account. + pub fn read_account(&self, params: ReadAccountParams) -> Option { + match params.account_number { + None => { + // Application number doesn't exist, return a default Account + Some(Account::new( + params.anchor_number, + params.origin.clone(), + // Default accounts have no name + None, + params.account_number, + )) + } + Some(account_number) => match self.stable_account_memory.get(&account_number) { + None => None, + Some(storable_account) => Some(Account::new( + params.anchor_number, + params.origin.clone(), + Some(storable_account.name.clone()), + Some(account_number), + )), + }, + } + } + + /// Updates an account. + /// If the account number exists, then updates that account. + /// If the account number doesn't exist, then gets or creates an application and creates and stores a default account. + #[allow(dead_code)] + pub fn update_account( + &mut self, + params: UpdateAccountParams, + ) -> Result { + match params.account_number { + Some(account_number) => self.update_existing_account(UpdateExistinAccountParams { + account_number, + name: params.name, + }), + None => { + // Default accounts are not stored by default. + // They are created only once they are updated. + self.create_default_account(CreateAccountParams { + anchor_number: params.anchor_number, + name: params.name, + origin: params.origin.clone(), + }) + } + } + } + + #[allow(dead_code)] + /// Used in `update_account` to update an existing account. + fn update_existing_account( + &mut self, + params: UpdateExistinAccountParams, + ) -> Result { + match self.stable_account_memory.get(¶ms.account_number) { + None => Err(StorageError::AccountNotFound { + account_number: params.account_number, + }), + Some(storable_account) => { + let mut storable_account = storable_account.clone(); + storable_account.name = params.name; + self.stable_account_memory + .insert(params.account_number, storable_account); + Ok(params.account_number) + } + } + } + + #[allow(dead_code)] + /// Used in `update_account` to create a default account. + /// Default account are not initially stored. They are stored when updated. + /// If the default account reference does not exist, it must be created. + /// If the default account reference exists, its account number must be updated. + fn create_default_account( + &mut self, + params: CreateAccountParams, + ) -> Result { + // Create and store the default account. + let new_account_number = self.allocate_account_number()?; + let storable_account = StorableAccount { + name: params.name.clone(), + // This was a default account which uses the anchor number for the seed. + seed_from_anchor: Some(params.anchor_number), + }; + self.stable_account_memory + .insert(new_account_number, storable_account); + + // Get or create an application number from the account's origin. + let application_number = + self.lookup_or_insert_application_number_with_origin(¶ms.origin); + // Update counters with one more account. + self.update_counters( + application_number, + params.anchor_number, + AccountType::Account, + )?; + + // Update the account references list. + let account_references_key = (params.anchor_number, application_number); + match self + .stable_account_reference_list_memory + .get(&account_references_key) + { + None => { + // If no list exists for this anchor & application, + // Create and insert the default account. + // This is because we don't create default accounts explicitly. + let new_ref = AccountReference { + account_number: Some(new_account_number), + last_used: None, + }; + self.stable_account_reference_list_memory + .insert(account_references_key, vec![new_ref].into()); + // One new account reference was created. + self.update_counters( + application_number, + params.anchor_number, + AccountType::AccountReference, + )?; + } + Some(existing_storable_list) => { + // If the list exists, update the default account reference with the new account number. + let mut refs_vec: Vec = existing_storable_list.into(); + let mut found_and_updated = false; + for r_mut in refs_vec.iter_mut() { + if r_mut.account_number.is_none() { + // Found the default account reference. + r_mut.account_number = Some(new_account_number); + found_and_updated = true; + break; + } + } + + // This could happen if the account was removed and now we try to update it. + if !found_and_updated { + return Err(StorageError::MissingAccount { + anchor_number: params.anchor_number, + name: params.name.clone(), + }); + } + self.stable_account_reference_list_memory + .insert(account_references_key, refs_vec.into()); + } + } + + Ok(new_account_number) + } + /// Make sure all the required metadata is recorded to stable memory. pub fn flush(&mut self) { let slice = unsafe { @@ -755,6 +1235,18 @@ impl Storage { "stable_identities".to_string(), self.stable_anchor_memory_wrapper.size(), ), + ( + "stable_accounts".to_string(), + self.stable_account_memory_wrapper.size(), + ), + ( + "stable_applications".to_string(), + self.stable_application_memory_wrapper.size(), + ), + ( + "stable_account_counter".to_string(), + self.stable_anchor_account_counter_memory_wrapper.size(), + ), ( "lookup_anchor_with_openid_credential".to_string(), self.lookup_anchor_with_openid_credential_memory_wrapper @@ -765,6 +1257,14 @@ impl Storage { self.lookup_anchor_with_device_credential_memory_wrapper .size(), ), + ( + "lookup_application_with_origin".to_string(), + self.lookup_application_with_origin_memory_wrapper.size(), + ), + ( + "stable_account_reference_list".to_string(), + self.stable_account_reference_list_memory_wrapper.size(), + ), ]) } } @@ -782,6 +1282,24 @@ pub enum StorageError { space_required: u64, space_available: u64, }, + AnchorNotFound { + anchor_number: AnchorNumber, + }, + ApplicationNotFound { + origin: FrontendHostname, + }, + MissingAccountName, + MissingAccount { + anchor_number: AnchorNumber, + name: String, + }, + AccountNotFound { + account_number: AccountNumber, + }, + OriginNotFoundForApplicationNumber { + application_number: ApplicationNumber, + }, + ErrorUpdatingAccountCounter, } impl fmt::Display for StorageError { @@ -810,6 +1328,36 @@ impl fmt::Display for StorageError { "attempted to store an entry of size {space_required} \ which is larger then the max allowed entry size {space_available}" ), + Self::AnchorNotFound { anchor_number } => { + write!( + f, + "StableAnchor not found for anchor number {}", + anchor_number + ) + } + Self::ApplicationNotFound { origin } => { + write!(f, "Application not found for origin {}", origin) + } + Self::MissingAccountName => write!(f, "Account name is missing"), + Self::MissingAccount { + anchor_number, + name, + } => { + write!( + f, + "Account not found for anchor number {} and name {}", + anchor_number, name + ) + } + Self::AccountNotFound { account_number } => { + write!(f, "Account not found for account number {}", account_number) + } + Self::OriginNotFoundForApplicationNumber { application_number } => write!( + f, + "Origin not found for application number {}", + application_number + ), + Self::ErrorUpdatingAccountCounter => write!(f, "Error updating account counter"), } } } diff --git a/src/internet_identity/src/storage/account.rs b/src/internet_identity/src/storage/account.rs new file mode 100644 index 0000000000..f214c1a965 --- /dev/null +++ b/src/internet_identity/src/storage/account.rs @@ -0,0 +1,153 @@ +use candid::CandidType; +use ic_stable_structures::{storable::Bound, Storable}; +use internet_identity_interface::internet_identity::types::{ + AccountNumber, AnchorNumber, FrontendHostname, Timestamp, +}; +use serde::Deserialize; +use std::borrow::Cow; + +#[cfg(test)] +mod tests; + +// API to manage accounts. +pub struct CreateAccountParams { + pub anchor_number: AnchorNumber, + pub name: String, + pub origin: FrontendHostname, +} + +pub struct UpdateAccountParams { + pub account_number: Option, + pub anchor_number: AnchorNumber, + pub name: String, + pub origin: FrontendHostname, +} + +pub struct UpdateExistinAccountParams { + pub account_number: AccountNumber, + pub name: String, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct ReadAccountParams { + pub account_number: Option, + pub anchor_number: AnchorNumber, + pub origin: FrontendHostname, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum AccountType { + AccountReference, + Account, +} + +// Types stored in memory. + +#[derive(Default, Clone, Debug, Deserialize, CandidType, serde::Serialize, PartialEq)] +pub struct AccountsCounter { + pub stored_accounts: u64, + pub stored_account_references: u64, +} + +impl AccountsCounter { + pub fn increment(&self, account_type: &AccountType) -> Self { + match account_type { + AccountType::AccountReference => Self { + stored_account_references: self.stored_account_references + 1, + stored_accounts: self.stored_accounts, + }, + AccountType::Account => Self { + stored_accounts: self.stored_accounts + 1, + stored_account_references: self.stored_account_references, + }, + } + } +} + +impl Storable for AccountsCounter { + fn to_bytes(&self) -> Cow<[u8]> { + Cow::Owned(serde_cbor::to_vec(&self).unwrap()) + } + + fn from_bytes(bytes: Cow<[u8]>) -> Self { + serde_cbor::from_slice(&bytes).unwrap() + } + + const BOUND: Bound = Bound::Unbounded; +} + +#[derive( + Clone, Debug, Deserialize, CandidType, serde::Serialize, Eq, PartialEq, Ord, PartialOrd, +)] +pub struct AccountReference { + pub account_number: Option, // None is the unreserved default account + pub last_used: Option, +} + +impl Storable for AccountReference { + fn to_bytes(&self) -> Cow<[u8]> { + Cow::Owned(serde_cbor::to_vec(&self).unwrap()) + } + + fn from_bytes(bytes: Cow<[u8]>) -> Self { + serde_cbor::from_slice(&bytes).unwrap() + } + + const BOUND: Bound = Bound::Unbounded; +} + +#[derive(Clone, Debug, Deserialize, serde::Serialize, Eq, PartialEq)] +pub struct StorableAccount { + pub name: String, + // Set if this is a default account + pub seed_from_anchor: Option, +} + +impl Storable for StorableAccount { + fn to_bytes(&self) -> Cow<[u8]> { + Cow::Owned(serde_cbor::to_vec(&self).unwrap()) + } + + fn from_bytes(bytes: Cow<[u8]>) -> Self { + serde_cbor::from_slice(&bytes).unwrap() + } + + const BOUND: Bound = Bound::Unbounded; +} + +// Types used internally to encapsulate business logic and data. + +#[derive(Clone, Debug, CandidType, Deserialize, Eq, PartialEq)] +pub struct Account { + pub account_number: Option, // None is unreserved default account + pub anchor_number: AnchorNumber, + pub origin: FrontendHostname, + pub last_used: Option, + pub name: Option, +} + +impl Account { + pub fn new( + anchor_number: AnchorNumber, + origin: FrontendHostname, + name: Option, + account_number: Option, + ) -> Account { + Self { + account_number, + anchor_number, + origin, + last_used: None, + name, + } + } + + // Used in tests (for now) + #[allow(dead_code)] + pub fn to_reference(&self) -> AccountReference { + AccountReference { + account_number: self.account_number, + last_used: self.last_used, + } + } +} diff --git a/src/internet_identity/src/storage/account/tests.rs b/src/internet_identity/src/storage/account/tests.rs new file mode 100644 index 0000000000..ec925ea3d3 --- /dev/null +++ b/src/internet_identity/src/storage/account/tests.rs @@ -0,0 +1,500 @@ +use crate::storage::account::{Account, AccountReference}; +use crate::storage::application::Application; +use crate::storage::{CreateAccountParams, ReadAccountParams, UpdateAccountParams}; +use crate::Storage; +use ic_stable_structures::VectorMemory; +use internet_identity_interface::internet_identity::types::{AnchorNumber, FrontendHostname}; + +use super::AccountsCounter; + +fn assert_empty_counters(storage: &Storage, anchor_number: AnchorNumber) { + assert_eq!( + storage.get_account_counter(anchor_number), + AccountsCounter::default() + ); + assert_eq!( + *storage.get_total_accounts_counter(), + AccountsCounter::default() + ); +} + +#[test] +fn should_create_additional_account() { + // Setup storage + let memory = VectorMemory::default(); + let mut storage = Storage::new((10_000, 3_784_873), memory); + + // 1. Define additional account parameters + let anchor_number: AnchorNumber = 10_000; + let origin: FrontendHostname = "https://some.origin".to_string(); + let account_name = "account name".to_string(); + + // 2. Additional account and application don't exist yet. + let read_params = ReadAccountParams { + account_number: Some(1), // First account created + anchor_number, + origin: origin.clone(), + }; + let additional_account_1 = storage.read_account(read_params.clone()); + assert!( + additional_account_1.is_none(), + "Additional account should not exist yet" + ); + assert!( + storage + .lookup_application_number_with_origin(&origin) + .is_none(), + "Application should not exist yet" + ); + assert_empty_counters(&storage, anchor_number); + + // 3. Create additional account + let new_account_params = CreateAccountParams { + anchor_number, + origin: origin.clone(), + name: account_name.clone(), + }; + storage + .create_additional_account(new_account_params) + .unwrap(); + + // 5. Check that read_account returns additional account, creates application and updates counters. + let additional_account = storage.read_account(read_params).unwrap(); + let expected_account = Account { + account_number: Some(1), + anchor_number, + origin: origin.clone(), + name: Some(account_name.clone()), + last_used: None, + }; + assert_eq!(additional_account, expected_account); + assert_eq!( + storage.lookup_application_with_origin(&origin).unwrap(), + Application { + origin: origin.clone(), + stored_accounts: 1, + stored_account_references: 2, + } + ); + assert_eq!( + storage.get_account_counter(anchor_number), + AccountsCounter { + stored_accounts: 1, + stored_account_references: 2, + } + ); + assert_eq!( + *storage.get_total_accounts_counter(), + AccountsCounter { + stored_accounts: 1, + stored_account_references: 2, + } + ); +} + +#[test] +fn should_list_accounts() { + // Setup storage + let memory = VectorMemory::default(); + let mut storage = Storage::new((10_000, 3_784_873), memory); + + // 1. Define additional account parameters + let anchor_number: AnchorNumber = 10_000; + let origin: FrontendHostname = "https://some.origin".to_string(); + let account_name = "account name".to_string(); + + // 2. Save anchor to stable memory + let anchor = storage.allocate_anchor().unwrap(); + storage.create(anchor).unwrap(); + + // 3. List accounts returns default account + let listed_accounts = storage.list_accounts(&anchor_number, &origin).unwrap(); + assert_eq!(listed_accounts.len(), 1); + assert!(listed_accounts[0].account_number.is_none()); + assert_empty_counters(&storage, anchor_number); + + // 4. Create new account + let new_account = CreateAccountParams { + anchor_number, + origin: origin.clone(), + name: account_name.clone(), + }; + let expected_additional_account_ref = AccountReference { + account_number: Some(1), + last_used: None, + }; + let expected_default_account_ref = AccountReference { + account_number: None, + last_used: None, + }; + storage.create_additional_account(new_account).unwrap(); + + // 5. List accounts returns default account + let listed_accounts = storage.list_accounts(&anchor_number, &origin).unwrap(); + + // 6. Assert that the list contains exactly two accounts and it matches the expected one + assert_eq!( + listed_accounts.len(), + 2, + "Expected exactly two accounts to be listed" + ); + assert_eq!( + listed_accounts[0], expected_default_account_ref, + "Default account reference is missing from the listed accounts." + ); + assert_eq!( + listed_accounts[1], expected_additional_account_ref, + "Additional account reference is missing from the listed accounts." + ); + assert_eq!( + storage.get_account_counter(anchor_number), + AccountsCounter { + stored_accounts: 1, + stored_account_references: 2, + } + ); + assert_eq!( + *storage.get_total_accounts_counter(), + AccountsCounter { + stored_accounts: 1, + stored_account_references: 2, + } + ); +} + +#[test] +fn should_list_all_identity_accounts() { + // Setup storage + let memory = VectorMemory::default(); + let mut storage = Storage::new((10_000, 3_784_873), memory); + + // 1. Define additional account parameters + let anchor_number: AnchorNumber = 10_000; + let account_name = "account name".to_string(); + let origin: FrontendHostname = "https://some.origin".to_string(); + let origin_2: FrontendHostname = "https://some-other.origin".to_string(); + + // 2. Save anchor to stable memory + let anchor = storage.allocate_anchor().unwrap(); + storage.create(anchor).unwrap(); + + // 3. List accounts returns default account + let listed_accounts = storage.list_identity_accounts(anchor_number); + assert_eq!(listed_accounts.len(), 0); + + // 4. Create additional account + let new_account_params = CreateAccountParams { + anchor_number, + origin: origin.clone(), + name: account_name.clone(), + }; + storage + .create_additional_account(new_account_params) + .unwrap(); + + // 5. List accounts returns default account + let listed_accounts = storage.list_identity_accounts(anchor_number); + // Default account + additional account for the origin application. + assert_eq!(listed_accounts.len(), 2); + + // 6. Create additional account + let new_account_params = CreateAccountParams { + anchor_number, + origin: origin_2.clone(), + name: account_name.clone(), + }; + storage + .create_additional_account(new_account_params) + .unwrap(); + + // 7. List accounts returns default account + let listed_accounts = storage.list_identity_accounts(anchor_number); + // Default account + additional account for the origin_2 application. + assert_eq!(listed_accounts.len(), 4); + + assert_eq!( + storage.get_account_counter(anchor_number), + AccountsCounter { + stored_accounts: 2, + stored_account_references: 4, + } + ); + assert_eq!( + *storage.get_total_accounts_counter(), + AccountsCounter { + stored_accounts: 2, + stored_account_references: 4, + } + ); +} + +#[test] +fn should_update_default_account() { + // Setup storage + let memory = VectorMemory::default(); + let mut storage = Storage::new((10_000, 3_784_873), memory); + + // 1. Define parameters + let anchor_number: AnchorNumber = 10_000; + let origin: FrontendHostname = "https://some.origin".to_string(); + let account_name = "account name".to_string(); + + // 2. Default account exists withuot creating it + let initial_accounts = storage.list_accounts(&anchor_number, &origin).unwrap(); + let expected_unreserved_account = AccountReference { + account_number: None, + last_used: None, + }; + assert_eq!(initial_accounts, vec![expected_unreserved_account]); + + // 3. Update default account + let updated_account_params = UpdateAccountParams { + anchor_number, + origin: origin.clone(), + name: account_name.clone(), + account_number: None, + }; + let new_account_number = storage.update_account(updated_account_params).unwrap(); + + // 4. Check that the default account has been created with the updated values. + let updated_accounts = storage.list_accounts(&anchor_number, &origin).unwrap(); + let expected_updated_account = AccountReference { + account_number: Some(new_account_number), + last_used: None, + }; + assert_eq!(updated_accounts, vec![expected_updated_account]); + assert_eq!( + storage.get_account_counter(anchor_number), + AccountsCounter { + stored_accounts: 1, + stored_account_references: 1, + } + ); + assert_eq!( + *storage.get_total_accounts_counter(), + AccountsCounter { + stored_accounts: 1, + stored_account_references: 1, + } + ); +} + +#[test] +fn should_update_additional_account() { + // Setup storage + let memory = VectorMemory::default(); + let mut storage = Storage::new((10_000, 3_784_873), memory); + + // 1. Define additional account parameters + let anchor_number: AnchorNumber = 10_000; + let origin: FrontendHostname = "https://some.origin".to_string(); + let account_name = "account name".to_string(); + let new_account_name = "new account name".to_string(); + let account_number = 1; + + // 2. Additional account and application don't exist yet. + let read_params = ReadAccountParams { + account_number: Some(account_number), // First account created is 1 + anchor_number, + origin: origin.clone(), + }; + let additional_account_1 = storage.read_account(read_params.clone()); + assert!( + additional_account_1.is_none(), + "Additional account should not exist yet" + ); + assert!( + storage + .lookup_application_number_with_origin(&origin) + .is_none(), + "Application should not exist yet" + ); + + // 3. Create additional account + let new_account_params = CreateAccountParams { + anchor_number, + origin: origin.clone(), + name: account_name.clone(), + }; + storage + .create_additional_account(new_account_params) + .unwrap(); + assert!(storage.read_account(read_params.clone()).is_some()); + + // 4. Update additional account + let updated_account_params = UpdateAccountParams { + anchor_number, + origin: origin.clone(), + name: new_account_name.clone(), + account_number: Some(1), + }; + let update_account_return_value = storage.update_account(updated_account_params).unwrap(); + + assert_eq!(update_account_return_value, account_number); + + // 5. Check that the additional account has been created with the updated values. + let updated_account = storage + .read_account(ReadAccountParams { + account_number: Some(update_account_return_value), + anchor_number, + origin: origin.clone(), + }) + .unwrap(); + let expected_updated_account = Account { + account_number: Some(update_account_return_value), + anchor_number, + origin: origin.clone(), + last_used: None, + name: Some(new_account_name), + }; + assert_eq!(updated_account, expected_updated_account); + assert_eq!( + storage.get_account_counter(anchor_number), + AccountsCounter { + stored_accounts: 1, + stored_account_references: 2, + } + ); + assert_eq!( + *storage.get_total_accounts_counter(), + AccountsCounter { + stored_accounts: 1, + stored_account_references: 2, + } + ); +} + +#[test] +fn should_count_accounts_different_anchors() { + // Setup storage + let memory = VectorMemory::default(); + let mut storage = Storage::new((10_000, 3_784_873), memory); + + // --- Anchor 1 --- + let anchor_1 = storage.allocate_anchor().unwrap(); + storage.create(anchor_1.clone()).unwrap(); + let anchor_number_1 = anchor_1.anchor_number(); + let origin_1: FrontendHostname = "https://origin1.com".to_string(); + let account_name_1 = "account_anchor1".to_string(); + + // List accounts for anchor 1 - should return 1 (default) + let accounts_anchor_1_initial = storage.list_accounts(&anchor_number_1, &origin_1).unwrap(); + assert_eq!( + accounts_anchor_1_initial.len(), + 1, + "Initial list for anchor 1 should have 1 account" + ); + assert!( + accounts_anchor_1_initial[0].account_number.is_none(), + "Initial account should be default" + ); + assert_empty_counters(&storage, anchor_number_1); + + // Check counters for anchor 1 - should be 0 + assert_eq!( + storage.get_account_counter(anchor_number_1), + AccountsCounter::default(), + "Counters for anchor 1 should be 0" + ); + assert_eq!( + *storage.get_total_accounts_counter(), + AccountsCounter::default(), + "Total counters should be 0" + ); + + // Create an additional account for anchor 1 + let create_params_1 = CreateAccountParams { + anchor_number: anchor_number_1, + origin: origin_1.clone(), + name: account_name_1.clone(), + }; + storage.create_additional_account(create_params_1).unwrap(); + + // List accounts for anchor 1 - should return 2 + let accounts_anchor_1_after_add = storage.list_accounts(&anchor_number_1, &origin_1).unwrap(); + assert_eq!( + accounts_anchor_1_after_add.len(), + 2, + "List for anchor 1 after additional account should have 2 accounts" + ); + + // Check counters for anchor 1 and total counters + let expected_counters_anchor_1 = AccountsCounter { + stored_accounts: 1, + stored_account_references: 2, + }; + assert_eq!( + storage.get_account_counter(anchor_number_1), + expected_counters_anchor_1, + "Counters for anchor 1 after additional account mismatch" + ); + assert_eq!( + *storage.get_total_accounts_counter(), + expected_counters_anchor_1, + "Total counters after anchor 1 additional account mismatch" + ); + + // --- Anchor 2 --- + let anchor_2 = storage.allocate_anchor().unwrap(); + storage.create(anchor_2.clone()).unwrap(); + let anchor_number_2 = anchor_2.anchor_number(); + let origin_2: FrontendHostname = "https://origin2.com".to_string(); + let account_name_2 = "account_anchor2".to_string(); + + // List accounts for anchor 2 - should return 1 (default) + let accounts_anchor_2_initial = storage.list_accounts(&anchor_number_2, &origin_2).unwrap(); + assert_eq!( + accounts_anchor_2_initial.len(), + 1, + "Initial list for anchor 2 should have 1 account" + ); + assert!( + accounts_anchor_2_initial[0].account_number.is_none(), + "Initial account for anchor 2 should be default" + ); + + // Check counters for anchor 2 - should be 0 (total counters still reflect anchor 1) + assert_eq!( + storage.get_account_counter(anchor_number_2), + AccountsCounter::default(), + "Counters for anchor 2 should be default 0" + ); + + // Create an additional account for anchor 2 + let create_params_2 = CreateAccountParams { + anchor_number: anchor_number_2, + origin: origin_2.clone(), + name: account_name_2.clone(), + }; + storage.create_additional_account(create_params_2).unwrap(); + + // List accounts for anchor 2 - should return 2 + let accounts_anchor_2_after_add = storage.list_accounts(&anchor_number_2, &origin_2).unwrap(); + assert_eq!( + accounts_anchor_2_after_add.len(), + 2, + "List for anchor 2 after additional account should have 2 accounts" + ); + + // Check counters for anchor 2 + let expected_counters_anchor_2 = AccountsCounter { + stored_accounts: 1, + stored_account_references: 2, + }; + assert_eq!( + storage.get_account_counter(anchor_number_2), + expected_counters_anchor_2, + "Counters for anchor 2 after additional account mismatch" + ); + + // Check total counters - should be sum of anchor 1 and anchor 2 + let expected_total_counters = AccountsCounter { + stored_accounts: 2, // 1 from anchor_1 + 1 from anchor_2 + stored_account_references: 4, // 2 from anchor_1 + 2 from anchor_2 + }; + assert_eq!( + *storage.get_total_accounts_counter(), + expected_total_counters, + "Total counters after anchor 2 additional account mismatch" + ); +} diff --git a/src/internet_identity/src/storage/application.rs b/src/internet_identity/src/storage/application.rs new file mode 100644 index 0000000000..3cf2de8b8b --- /dev/null +++ b/src/internet_identity/src/storage/application.rs @@ -0,0 +1,61 @@ +use ic_stable_structures::{storable::Bound, Storable}; +use internet_identity_interface::internet_identity::types::FrontendHostname; +use serde::Deserialize; +use sha2::{Digest, Sha256}; +use std::borrow::Cow; + +#[derive(Clone, Debug, Deserialize, serde::Serialize, Eq, PartialEq, Ord, PartialOrd)] +pub struct Application { + pub origin: FrontendHostname, + pub stored_accounts: u64, + pub stored_account_references: u64, +} + +impl Storable for Application { + fn to_bytes(&self) -> Cow<[u8]> { + Cow::Owned(serde_cbor::to_vec(&self).unwrap()) + } + + fn from_bytes(bytes: Cow<[u8]>) -> Self { + serde_cbor::from_slice(&bytes).unwrap() + } + + const BOUND: Bound = Bound::Unbounded; +} + +#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] +pub struct OriginHash { + hash: [u8; 8], +} + +impl Storable for OriginHash { + fn to_bytes(&self) -> Cow<[u8]> { + Cow::Owned(self.hash.to_vec()) + } + + fn from_bytes(bytes: Cow<[u8]>) -> Self { + Self { + hash: bytes.as_ref().try_into().unwrap(), + } + } + + const BOUND: Bound = Bound::Bounded { + max_size: 8, + is_fixed_size: true, + }; +} + +impl OriginHash { + pub fn from_origin(origin: &FrontendHostname) -> Self { + let mut hasher = Sha256::new(); + hasher.update(origin.as_bytes()); + let full_hash_result = hasher.finalize(); + // Truncate the 32-byte SHA-256 hash to the first 8 bytes. + let truncated_hash_slice: &[u8] = &full_hash_result[0..8]; + let hash_8_bytes: [u8; 8] = truncated_hash_slice + .try_into() + .expect("Failed to truncate SHA256 hash to 8 bytes; slice length should be 8."); + + Self { hash: hash_8_bytes } + } +} diff --git a/src/internet_identity/src/storage/storable_account_reference_list.rs b/src/internet_identity/src/storage/storable_account_reference_list.rs new file mode 100644 index 0000000000..7630203d02 --- /dev/null +++ b/src/internet_identity/src/storage/storable_account_reference_list.rs @@ -0,0 +1,42 @@ +use super::account::AccountReference; +use candid::CandidType; +use ic_stable_structures::storable::Bound; +use ic_stable_structures::Storable; +use serde::Deserialize; +use std::borrow::Cow; + +/// Vectors are not supported yet in ic-stable-structures, this file +/// implements a struct to wrap this vector so it can be stored. +#[derive(Deserialize, CandidType, Clone, Ord, Eq, PartialEq, PartialOrd, Default)] +pub struct StorableAccountReferenceList(Vec); + +impl From for Vec { + fn from(value: StorableAccountReferenceList) -> Self { + value.0 + } +} + +impl From> for StorableAccountReferenceList { + fn from(value: Vec) -> Self { + StorableAccountReferenceList(value) + } +} + +impl Storable for StorableAccountReferenceList { + fn to_bytes(&self) -> Cow<[u8]> { + let mut candid = candid::encode_one(self) + .expect("Failed to serialize StorableAccountReferenceList to candid"); + let mut buf = (candid.len() as u16).to_le_bytes().to_vec(); // 2 bytes for length + buf.append(&mut candid); + Cow::Owned(buf) + } + + fn from_bytes(bytes: Cow<[u8]>) -> Self { + let length = u16::from_le_bytes(bytes[..2].try_into().unwrap()) as usize; + + candid::decode_one(&bytes[2..length + 2]) + .expect("Failed to deserialize StorableAccountReferenceList from candid") + } + + const BOUND: Bound = Bound::Unbounded; +} diff --git a/src/internet_identity_interface/src/internet_identity/types.rs b/src/internet_identity_interface/src/internet_identity/types.rs index 25f9b99a73..b2ded1b8a5 100644 --- a/src/internet_identity_interface/src/internet_identity/types.rs +++ b/src/internet_identity_interface/src/internet_identity/types.rs @@ -10,6 +10,7 @@ pub type UserKey = PublicKey; pub type SessionKey = PublicKey; pub type CanisterSigPublicKeyDer = PublicKey; pub type FrontendHostname = String; +pub type ApplicationNumber = u64; pub type Timestamp = u64; // in nanos since epoch pub type Signature = ByteBuf; pub type DeviceVerificationCode = String; @@ -320,8 +321,8 @@ pub struct DeviceKeyWithAnchor { } #[derive(Clone, Debug, CandidType, Deserialize, Eq, PartialEq)] -pub struct Account { - pub account_number: Option, // Null is unreserved default account +pub struct AccountInfo { + pub account_number: Option, // None is the unreserved default account pub origin: FrontendHostname, pub last_used: Option, pub name: Option,