From 8540902716af79fd375eff3a749b84a384decdcc Mon Sep 17 00:00:00 2001 From: micbakos-rdx <125959264+micbakos-rdx@users.noreply.github.com> Date: Tue, 15 Oct 2024 10:22:28 +0300 Subject: [PATCH] Multi-signatures collector integration (#235) * Transfer SignaturesCollectorDependencies into sargon * WIP * Add passphrase factor source kind * Add passphrase factor source * Add FactorSourceIDFromHash samples * Implement custom HDFactorSourceIdFromHash to keep track of the factor source id and the mnemonic it was created with * Remove custom struct * Add petition_for_factors * Add Implement a general role with hd factor instances * Add securified variant in EntitySecurityState * Fix tests in petition_for_entity * Transfer petition_for_transaction * WIP more files * Transfer SignaturesCollectorPreprocessor * Implement transaction intent initializer that accepts account and identity addresses requiring auth * Allow signing inputs in tests from associated mnemonics * Transfer signatures collector tests * Transfer remaining signatures_collector tests * Include integration tests * Rebase with main * Remove Ord impl for Persona and Account * Address todo comments * Format code * Fix errors in swift tests * Remove comment * Fix ide errors * Add doc * Add doc * Remove doc * Associate entity samples with entity address samples * Add doc * Rename * Remove doc * Destructure value * Hide from function * Rename function * Rename general role samples * Create HDFactorInstance directly from derivation path's params * Rename parameter * Expose role kind * Add new constructor * Return result when creating a new role * Add apple tests for passphrase factor source * Add doc * Bump version * Fix test * Run clippy * Run fmt --- Cargo.lock | 38 +- .../PassphraseFactorSource+Functions.swift | 18 + .../BIP39/Mnemonic+SampleValues.swift | 8 +- .../PassphraseFactorSource+SampleValues.swift | 19 + .../FactorSource+Swiftified.swift | 9 + .../PassphraseFactorSource+Swiftified.swift | 38 + .../Sargon/Protocols/EntityProtocol.swift | 6 + .../Profile/EntitySecurityStateTests.swift | 16 - .../Factor/PassphraseFactorSourceTests.swift | 80 + apple/Tests/Utils/EntityProtocolTest.swift | 2 + crates/sargon/Cargo.toml | 5 +- crates/sargon/src/core/error/common_error.rs | 18 + crates/sargon/src/core/hash.rs | 15 + .../identified_vec_of_query.rs | 2 +- .../sargon/src/core/types/collections/just.rs | 74 + .../sargon/src/core/types/collections/mod.rs | 2 + crates/sargon/src/core/types/decimal192.rs | 20 +- .../src/core/types/decimal192_uniffi_fn.rs | 4 +- .../sargon/src/core/utils/image_url_utils.rs | 2 +- .../core/utils/image_url_utils_uniffi_fn.rs | 2 +- .../transaction/status/transaction_status.rs | 4 +- .../bip39/mnemonic.rs | 25 + .../bip39/mnemonic_uniffi_fn.rs | 14 +- .../derivation/mnemonic_with_passphrase.rs | 28 + crates/sargon/src/lib.rs | 6 + .../sargon/src/profile/logic/account/mod.rs | 2 + .../profile/logic/account/query_personas.rs | 58 + .../profile_network_details.rs | 1 + .../profile/logic/profile_next_derivation.rs | 16 +- .../src/profile/mfa/mfa_factor_sources/mod.rs | 2 + .../passphrase_factor_source/mod.rs | 5 + .../passphrase_factor_source.rs | 178 ++ .../passphrase_factor_source_uniffi_fn.rs | 53 + .../decl_security_structure_of.rs | 34 +- ...security_structure_of_factor_source_ids.rs | 3 + .../security_structure_of_factor_sources.rs | 26 +- .../supporting_types/account_or_persona.rs | 32 +- .../profile/v100/entity/account/account.rs | 26 - .../profile/v100/entity/persona/persona.rs | 26 - .../entity_security_state.rs | 164 ++ .../src/profile/v100/factors/factor_source.rs | 58 + .../factors/factor_source_id_from_hash.rs | 64 +- .../v100/factors/factor_source_kind.rs | 76 +- .../hd_transaction_signing_factor_instance.rs | 3 +- ...rarchical_deterministic_factor_instance.rs | 26 + crates/sargon/src/signing/collector/mod.rs | 13 + .../signatures_collecting_continuation.rs | 14 + .../signing/collector/signatures_collector.rs | 2314 +++++++++++++++++ .../signatures_collector_dependencies.rs | 36 + .../signatures_collector_preprocessor.rs | 116 + .../collector/signatures_collector_state.rs | 14 + .../signing_finish_early_strategy.rs | 121 + .../host_interaction/interactors/mod.rs | 7 + .../mono_factor_sign_interactor.rs | 23 + .../poly_factor_sign_interactor.rs | 27 + .../interactors/sign_interactor.rs | 19 + .../src/signing/host_interaction/mod.rs | 9 + .../signing/host_interaction/requests/mod.rs | 9 + .../requests/mono_factor_sign_request.rs | 33 + .../mono_factor_sign_request_input.rs | 94 + .../requests/poly_factor_sign_request.rs | 91 + .../transaction_sign_request_input.rs | 117 + .../host_interaction/sign_interactors.rs | 6 + .../signing/host_interaction/sign_response.rs | 31 + crates/sargon/src/signing/mod.rs | 18 + .../petition_types/factor_list_kind.rs | 6 + .../general_role_with_hd_factor_instance.rs | 196 ++ .../sargon/src/signing/petition_types/mod.rs | 18 + .../petition_types/petition_for_entity.rs | 704 +++++ .../factor_source_referencing.rs | 22 + .../petition_for_factors_types/mod.rs | 8 + .../neglected_factor_instance.rs | 102 + .../petition_for_factors/mod.rs | 16 + .../petition_for_factors.rs | 220 ++ .../petition_for_factors_input.rs | 106 + .../petition_for_factors_state.rs | 197 ++ .../petition_for_factors_state_snapshot.rs | 96 + .../petition_for_factors_status.rs | 119 + .../petition_for_factors_sub_state.rs | 39 + .../petition_for_transaction.rs | 349 +++ .../signing/petition_types/petition_status.rs | 39 + .../src/signing/petition_types/petitions.rs | 266 ++ .../src/signing/petition_types/role_kind.rs | 6 + .../maybe_signed_transactions.rs | 337 +++ .../signing/signatures_outecome_types/mod.rs | 9 + .../petition_transaction_outcome.rs | 56 + .../sign_with_factors_outcome.rs | 61 + .../signatures_outcome.rs | 182 ++ .../src/signing/testing/interactors/mod.rs | 10 + .../testing/interactors/test_interactor.rs | 17 + .../interactors/test_parallel_interactor.rs | 82 + .../interactors/test_serial_interactor.rs | 71 + crates/sargon/src/signing/testing/mod.rs | 13 + .../src/signing/testing/simulated_user.rs | 190 ++ .../test_signature_collecting_interactors.rs | 27 + .../testing/test_signatures_collector.rs | 128 + crates/sargon/src/signing/tx_to_sign.rs | 84 + .../system/sargon_os/profile_state_holder.rs | 4 +- .../src/types/factor_sources_of_kind.rs | 117 + crates/sargon/src/types/hd_signature.rs | 119 + crates/sargon/src/types/hd_signature_input.rs | 75 + .../types/invalid_transaction_if_neglected.rs | 98 + crates/sargon/src/types/mod.rs | 15 + crates/sargon/src/types/owned_types/mod.rs | 5 + crates/sargon/src/types/owned_types/owned.rs | 26 + .../owned_types/owned_factor_instance.rs | 41 + .../access_controller_address_samples.rs | 31 + .../types/samples/account_address_samples.rs | 11 + .../src/types/samples/account_samples.rs | 213 ++ .../samples/factor_source_ids_with_samples.rs | 57 + .../types/samples/factor_source_samples.rs | 93 + ...al_role_with_hd_factor_instance_samples.rs | 98 + ...l_deterministic_factor_instance_samples.rs | 81 + .../types/samples/identity_address_samples.rs | 11 + crates/sargon/src/types/samples/mod.rs | 23 + .../src/types/samples/persona_samples.rs | 180 ++ .../src/types/samples/profile_samples.rs | 52 + .../samples/transaction_intent_samples.rs | 116 + .../low_level/transaction_intent.rs | 4 + .../transaction_manifest_execution_summary.rs | 2 +- crates/sargon/tests/integration/main.rs | 225 +- .../os/driver/AndroidStorageDriverTest.kt | 5 +- .../sargon/extensions/FactorSource.kt | 6 + 123 files changed, 9716 insertions(+), 188 deletions(-) create mode 100644 apple/Sources/Sargon/Extensions/Methods/Profile/Factor/PassphraseFactorSource+Functions.swift create mode 100644 apple/Sources/Sargon/Extensions/SampleValues/Profile/Factor/FactorSource/PassphraseFactorSource+SampleValues.swift create mode 100644 apple/Sources/Sargon/Extensions/Swiftified/Profile/Factor/FactorSource/PassphraseFactorSource+Swiftified.swift create mode 100644 apple/Tests/TestCases/Profile/Factor/PassphraseFactorSourceTests.swift create mode 100644 crates/sargon/src/core/types/collections/just.rs create mode 100644 crates/sargon/src/profile/logic/account/query_personas.rs create mode 100644 crates/sargon/src/profile/mfa/mfa_factor_sources/passphrase_factor_source/mod.rs create mode 100644 crates/sargon/src/profile/mfa/mfa_factor_sources/passphrase_factor_source/passphrase_factor_source.rs create mode 100644 crates/sargon/src/profile/mfa/mfa_factor_sources/passphrase_factor_source/passphrase_factor_source_uniffi_fn.rs create mode 100644 crates/sargon/src/signing/collector/mod.rs create mode 100644 crates/sargon/src/signing/collector/signatures_collecting_continuation.rs create mode 100644 crates/sargon/src/signing/collector/signatures_collector.rs create mode 100644 crates/sargon/src/signing/collector/signatures_collector_dependencies.rs create mode 100644 crates/sargon/src/signing/collector/signatures_collector_preprocessor.rs create mode 100644 crates/sargon/src/signing/collector/signatures_collector_state.rs create mode 100644 crates/sargon/src/signing/collector/signing_finish_early_strategy.rs create mode 100644 crates/sargon/src/signing/host_interaction/interactors/mod.rs create mode 100644 crates/sargon/src/signing/host_interaction/interactors/mono_factor_sign_interactor.rs create mode 100644 crates/sargon/src/signing/host_interaction/interactors/poly_factor_sign_interactor.rs create mode 100644 crates/sargon/src/signing/host_interaction/interactors/sign_interactor.rs create mode 100644 crates/sargon/src/signing/host_interaction/mod.rs create mode 100644 crates/sargon/src/signing/host_interaction/requests/mod.rs create mode 100644 crates/sargon/src/signing/host_interaction/requests/mono_factor_sign_request.rs create mode 100644 crates/sargon/src/signing/host_interaction/requests/mono_factor_sign_request_input.rs create mode 100644 crates/sargon/src/signing/host_interaction/requests/poly_factor_sign_request.rs create mode 100644 crates/sargon/src/signing/host_interaction/requests/transaction_sign_request_input.rs create mode 100644 crates/sargon/src/signing/host_interaction/sign_interactors.rs create mode 100644 crates/sargon/src/signing/host_interaction/sign_response.rs create mode 100644 crates/sargon/src/signing/mod.rs create mode 100644 crates/sargon/src/signing/petition_types/factor_list_kind.rs create mode 100644 crates/sargon/src/signing/petition_types/general_role_with_hd_factor_instance.rs create mode 100644 crates/sargon/src/signing/petition_types/mod.rs create mode 100644 crates/sargon/src/signing/petition_types/petition_for_entity.rs create mode 100644 crates/sargon/src/signing/petition_types/petition_for_factors_types/factor_source_referencing.rs create mode 100644 crates/sargon/src/signing/petition_types/petition_for_factors_types/mod.rs create mode 100644 crates/sargon/src/signing/petition_types/petition_for_factors_types/neglected_factor_instance.rs create mode 100644 crates/sargon/src/signing/petition_types/petition_for_factors_types/petition_for_factors/mod.rs create mode 100644 crates/sargon/src/signing/petition_types/petition_for_factors_types/petition_for_factors/petition_for_factors.rs create mode 100644 crates/sargon/src/signing/petition_types/petition_for_factors_types/petition_for_factors/petition_for_factors_input.rs create mode 100644 crates/sargon/src/signing/petition_types/petition_for_factors_types/petition_for_factors/petition_for_factors_state.rs create mode 100644 crates/sargon/src/signing/petition_types/petition_for_factors_types/petition_for_factors/petition_for_factors_state_snapshot.rs create mode 100644 crates/sargon/src/signing/petition_types/petition_for_factors_types/petition_for_factors/petition_for_factors_status.rs create mode 100644 crates/sargon/src/signing/petition_types/petition_for_factors_types/petition_for_factors/petition_for_factors_sub_state.rs create mode 100644 crates/sargon/src/signing/petition_types/petition_for_transaction.rs create mode 100644 crates/sargon/src/signing/petition_types/petition_status.rs create mode 100644 crates/sargon/src/signing/petition_types/petitions.rs create mode 100644 crates/sargon/src/signing/petition_types/role_kind.rs create mode 100644 crates/sargon/src/signing/signatures_outecome_types/maybe_signed_transactions.rs create mode 100644 crates/sargon/src/signing/signatures_outecome_types/mod.rs create mode 100644 crates/sargon/src/signing/signatures_outecome_types/petition_transaction_outcome.rs create mode 100644 crates/sargon/src/signing/signatures_outecome_types/sign_with_factors_outcome.rs create mode 100644 crates/sargon/src/signing/signatures_outecome_types/signatures_outcome.rs create mode 100644 crates/sargon/src/signing/testing/interactors/mod.rs create mode 100644 crates/sargon/src/signing/testing/interactors/test_interactor.rs create mode 100644 crates/sargon/src/signing/testing/interactors/test_parallel_interactor.rs create mode 100644 crates/sargon/src/signing/testing/interactors/test_serial_interactor.rs create mode 100644 crates/sargon/src/signing/testing/mod.rs create mode 100644 crates/sargon/src/signing/testing/simulated_user.rs create mode 100644 crates/sargon/src/signing/testing/test_signature_collecting_interactors.rs create mode 100644 crates/sargon/src/signing/testing/test_signatures_collector.rs create mode 100644 crates/sargon/src/signing/tx_to_sign.rs create mode 100644 crates/sargon/src/types/factor_sources_of_kind.rs create mode 100644 crates/sargon/src/types/hd_signature.rs create mode 100644 crates/sargon/src/types/hd_signature_input.rs create mode 100644 crates/sargon/src/types/invalid_transaction_if_neglected.rs create mode 100644 crates/sargon/src/types/owned_types/mod.rs create mode 100644 crates/sargon/src/types/owned_types/owned.rs create mode 100644 crates/sargon/src/types/owned_types/owned_factor_instance.rs create mode 100644 crates/sargon/src/types/samples/access_controller_address_samples.rs create mode 100644 crates/sargon/src/types/samples/account_address_samples.rs create mode 100644 crates/sargon/src/types/samples/account_samples.rs create mode 100644 crates/sargon/src/types/samples/factor_source_ids_with_samples.rs create mode 100644 crates/sargon/src/types/samples/factor_source_samples.rs create mode 100644 crates/sargon/src/types/samples/general_role_with_hd_factor_instance_samples.rs create mode 100644 crates/sargon/src/types/samples/hierarchical_deterministic_factor_instance_samples.rs create mode 100644 crates/sargon/src/types/samples/identity_address_samples.rs create mode 100644 crates/sargon/src/types/samples/mod.rs create mode 100644 crates/sargon/src/types/samples/persona_samples.rs create mode 100644 crates/sargon/src/types/samples/profile_samples.rs create mode 100644 crates/sargon/src/types/samples/transaction_intent_samples.rs diff --git a/Cargo.lock b/Cargo.lock index c57fa8abd..a3e6efcd3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -79,7 +79,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "once_cell", + "once_cell 1.19.0 (registry+https://github.com/rust-lang/crates.io-index)", "version_check", "zerocopy", ] @@ -277,7 +277,7 @@ dependencies = [ "async-lock", "blocking", "futures-lite", - "once_cell", + "once_cell 1.19.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -329,7 +329,7 @@ dependencies = [ "kv-log-macro", "log", "memchr", - "once_cell", + "once_cell 1.19.0 (registry+https://github.com/rust-lang/crates.io-index)", "pin-project-lite", "pin-utils", "slab", @@ -1808,7 +1808,7 @@ dependencies = [ "cfg-if", "ecdsa", "elliptic-curve", - "once_cell", + "once_cell 1.19.0 (registry+https://github.com/rust-lang/crates.io-index)", "sha2 0.10.8", "signature 2.2.0", ] @@ -1939,7 +1939,7 @@ dependencies = [ "crossbeam-epoch", "crossbeam-utils", "num_cpus", - "once_cell", + "once_cell 1.19.0 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot", "rustc_version", "scheduled-thread-pool", @@ -2037,6 +2037,11 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "once_cell" +version = "1.19.0" +source = "git+https://github.com/matklad/once_cell/?rev=c48d3c2c01de926228aea2ac1d03672b4ce160c1#c48d3c2c01de926228aea2ac1d03672b4ce160c1" + [[package]] name = "opaque-debug" version = "0.3.1" @@ -2053,7 +2058,7 @@ dependencies = [ "cfg-if", "foreign-types", "libc", - "once_cell", + "once_cell 1.19.0 (registry+https://github.com/rust-lang/crates.io-index)", "openssl-macros", "openssl-sys", ] @@ -2765,7 +2770,7 @@ dependencies = [ "log", "mime", "native-tls", - "once_cell", + "once_cell 1.19.0 (registry+https://github.com/rust-lang/crates.io-index)", "percent-encoding", "pin-project-lite", "rustls-pemfile", @@ -2860,7 +2865,7 @@ dependencies = [ [[package]] name = "sargon" -version = "1.1.28" +version = "1.1.29" dependencies = [ "actix-rt", "aes-gcm", @@ -2883,6 +2888,7 @@ dependencies = [ "itertools 0.12.0", "k256 0.13.3 (git+https://github.com/RustCrypto/elliptic-curves?rev=e158ce5cf0e9acee2fd76aff2a628334f5c771e5)", "log", + "once_cell 1.19.0 (git+https://github.com/matklad/once_cell/?rev=c48d3c2c01de926228aea2ac1d03672b4ce160c1)", "paste 1.0.14", "pretty_assertions", "pretty_env_logger", @@ -3512,7 +3518,7 @@ checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" dependencies = [ "cfg-if", "fastrand", - "once_cell", + "once_cell 1.19.0 (registry+https://github.com/rust-lang/crates.io-index)", "rustix", "windows-sys 0.59.0", ] @@ -3741,7 +3747,7 @@ version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ - "once_cell", + "once_cell 1.19.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -3854,7 +3860,7 @@ dependencies = [ "glob", "goblin", "heck 0.5.0", - "once_cell", + "once_cell 1.19.0 (registry+https://github.com/rust-lang/crates.io-index)", "paste 1.0.15", "serde", "textwrap", @@ -3892,7 +3898,7 @@ dependencies = [ "bytes", "camino 1.1.9", "log", - "once_cell", + "once_cell 1.19.0 (registry+https://github.com/rust-lang/crates.io-index)", "paste 1.0.15", "static_assertions", ] @@ -3905,7 +3911,7 @@ dependencies = [ "bincode", "camino 1.1.9", "fs-err", - "once_cell", + "once_cell 1.19.0 (registry+https://github.com/rust-lang/crates.io-index)", "proc-macro2", "quote", "serde", @@ -3934,7 +3940,7 @@ dependencies = [ "camino 1.1.9", "cargo_metadata 0.15.4", "fs-err", - "once_cell", + "once_cell 1.19.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -4051,7 +4057,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" dependencies = [ "cfg-if", - "once_cell", + "once_cell 1.19.0 (registry+https://github.com/rust-lang/crates.io-index)", "wasm-bindgen-macro", ] @@ -4063,7 +4069,7 @@ checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" dependencies = [ "bumpalo", "log", - "once_cell", + "once_cell 1.19.0 (registry+https://github.com/rust-lang/crates.io-index)", "proc-macro2", "quote", "syn 2.0.77", diff --git a/apple/Sources/Sargon/Extensions/Methods/Profile/Factor/PassphraseFactorSource+Functions.swift b/apple/Sources/Sargon/Extensions/Methods/Profile/Factor/PassphraseFactorSource+Functions.swift new file mode 100644 index 000000000..e708323c7 --- /dev/null +++ b/apple/Sources/Sargon/Extensions/Methods/Profile/Factor/PassphraseFactorSource+Functions.swift @@ -0,0 +1,18 @@ +// +// File.swift +// +// +// Created by Alexander Cyon on 2024-06-02. +// + +import Foundation +import SargonUniFFI + +extension PassphraseFactorSource { + + public init( + mnemonicWithPassphrase mwp: MnemonicWithPassphrase + ) { + self = newPassphraseFactorSourceFromMnemonicWithPassphrase(mwp: mwp) + } +} diff --git a/apple/Sources/Sargon/Extensions/SampleValues/Crypto/Derivation/BIP39/Mnemonic+SampleValues.swift b/apple/Sources/Sargon/Extensions/SampleValues/Crypto/Derivation/BIP39/Mnemonic+SampleValues.swift index f97ea9d8d..f2ff5b549 100644 --- a/apple/Sources/Sargon/Extensions/SampleValues/Crypto/Derivation/BIP39/Mnemonic+SampleValues.swift +++ b/apple/Sources/Sargon/Extensions/SampleValues/Crypto/Derivation/BIP39/Mnemonic+SampleValues.swift @@ -17,7 +17,9 @@ extension Mnemonic { public static let sampleOffDeviceMnemonicOther: Self = newMnemonicSampleOffDeviceOther() public static let sampleSecurityQuestions: Self = newMnemonicSampleSecurityQuestions() public static let sampleSecurityQuestionsOther: Self = newMnemonicSampleSecurityQuestionsOther() - + public static let samplePassphrase: Self = newMnemonicSamplePassphrase() + public static let samplePassphraseOther: Self = newMnemonicSamplePassphraseOther() + public static let sample24ZooVote: Self = try! Self(phrase: "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo vote") public static let sampleValues: [Self] = [ @@ -32,7 +34,9 @@ extension Mnemonic { .sampleOffDeviceMnemonic, .sampleOffDeviceMnemonicOther, .sampleSecurityQuestions, - .sampleSecurityQuestionsOther + .sampleSecurityQuestionsOther, + .samplePassphrase, + .samplePassphraseOther ] } #endif // DEBUG diff --git a/apple/Sources/Sargon/Extensions/SampleValues/Profile/Factor/FactorSource/PassphraseFactorSource+SampleValues.swift b/apple/Sources/Sargon/Extensions/SampleValues/Profile/Factor/FactorSource/PassphraseFactorSource+SampleValues.swift new file mode 100644 index 000000000..ad13218b2 --- /dev/null +++ b/apple/Sources/Sargon/Extensions/SampleValues/Profile/Factor/FactorSource/PassphraseFactorSource+SampleValues.swift @@ -0,0 +1,19 @@ +// +// PassphraseFactorSource+SampleValues.swift +// +// +// Created by Michael Bakogiannis on 7/10/24. +// + +import Foundation +import SargonUniFFI + +#if DEBUG +extension PassphraseFactorSource { + public static let sample: Self = newPassphraseFactorSourceSample() + + public static let sampleOther: Self = newPassphraseFactorSourceSampleOther() +} + +#endif // DEBUG + diff --git a/apple/Sources/Sargon/Extensions/Swiftified/Profile/Factor/FactorSource/FactorSource+Swiftified.swift b/apple/Sources/Sargon/Extensions/Swiftified/Profile/Factor/FactorSource/FactorSource+Swiftified.swift index 02ec0ced7..883570cad 100644 --- a/apple/Sources/Sargon/Extensions/Swiftified/Profile/Factor/FactorSource/FactorSource+Swiftified.swift +++ b/apple/Sources/Sargon/Extensions/Swiftified/Profile/Factor/FactorSource/FactorSource+Swiftified.swift @@ -18,6 +18,7 @@ extension FactorSource: Identifiable { case let .ledger(value): value.id.asGeneral case let .offDeviceMnemonic(value): value.id.asGeneral case let .trustedContact(value): value.id.asGeneral + case let .passphrase(value): value.id.asGeneral } } } @@ -35,6 +36,7 @@ extension FactorSource: BaseFactorSourceProtocol { case let .arculusCard(value): value.factorSourceKind case let .offDeviceMnemonic(value): value.factorSourceKind case let .trustedContact(value): value.factorSourceKind + case let .passphrase(value): value.factorSourceKind } } @@ -47,6 +49,7 @@ extension FactorSource: BaseFactorSourceProtocol { case let .arculusCard(value): value.common case let .offDeviceMnemonic(value): value.common case let .trustedContact(value): value.common + case let .passphrase(value): value.common } } set { @@ -69,6 +72,9 @@ extension FactorSource: BaseFactorSourceProtocol { case var .trustedContact(source): source.common = newValue self = .trustedContact(value: source) + case var .passphrase(source): + source.common = newValue + self = .passphrase(value: source) } } } @@ -112,4 +118,7 @@ extension FactorSource: BaseFactorSourceProtocol { public var asTrustedContact: TrustedContactFactorSource? { extract() } + public var asPassphrase: PassphraseFactorSource? { + extract() + } } diff --git a/apple/Sources/Sargon/Extensions/Swiftified/Profile/Factor/FactorSource/PassphraseFactorSource+Swiftified.swift b/apple/Sources/Sargon/Extensions/Swiftified/Profile/Factor/FactorSource/PassphraseFactorSource+Swiftified.swift new file mode 100644 index 000000000..34da7e75a --- /dev/null +++ b/apple/Sources/Sargon/Extensions/Swiftified/Profile/Factor/FactorSource/PassphraseFactorSource+Swiftified.swift @@ -0,0 +1,38 @@ +// +// PassphraseFactorSource+Swiftified.swift +// +// +// Created by Michael Bakogiannis on 7/10/24. +// + +import Foundation +import SargonUniFFI + +extension PassphraseFactorSource: SargonModel {} +extension PassphraseFactorSource: Identifiable { + public typealias ID = FactorSourceIDFromHash +} + +extension PassphraseFactorSource: FactorSourceProtocol { + public static let kind: FactorSourceKind = .passphrase + + public static func extract(from someFactorSource: some BaseFactorSourceProtocol) -> Self? { + guard case let .passphrase(factorSource) = someFactorSource.asGeneral else { return nil } + return factorSource + } + + public var asGeneral: FactorSource { + .passphrase(value: self) + } + + public var factorSourceID: FactorSourceID { + id.asGeneral + } + + public var factorSourceKind: FactorSourceKind { + .passphrase + } + + public var supportsOlympia: Bool { asGeneral.supportsOlympia } + public var supportsBabylon: Bool { asGeneral.supportsBabylon } +} diff --git a/apple/Sources/Sargon/Protocols/EntityProtocol.swift b/apple/Sources/Sargon/Protocols/EntityProtocol.swift index fe8437f60..4807ebf94 100644 --- a/apple/Sources/Sargon/Protocols/EntityProtocol.swift +++ b/apple/Sources/Sargon/Protocols/EntityProtocol.swift @@ -45,6 +45,8 @@ extension EntityBaseProtocol { factorInstances.insert(authSigning) } return factorInstances + case .securified(value: let value): + return [] } } @@ -52,6 +54,8 @@ extension EntityBaseProtocol { switch securityState { case let .unsecured(unsecuredEntityControl): unsecuredEntityControl.authenticationSigning != nil + case .securified(value: let value): + false // TODO handle that in the future } } @@ -64,6 +68,8 @@ extension EntityBaseProtocol { } return factorSourceID + case .securified(value: _): + return nil // TODO handle that in the future } } } diff --git a/apple/Tests/TestCases/Profile/EntitySecurityStateTests.swift b/apple/Tests/TestCases/Profile/EntitySecurityStateTests.swift index bf86eacdc..57bbfe75e 100644 --- a/apple/Tests/TestCases/Profile/EntitySecurityStateTests.swift +++ b/apple/Tests/TestCases/Profile/EntitySecurityStateTests.swift @@ -12,19 +12,3 @@ import SargonUniFFI import XCTest final class EntitySecurityStateTests: Test {} - -#if DEBUG -extension EntitySecurityState { - public var unsecured: UnsecuredEntityControl { - get { - switch self { - case let .unsecured(value): - return value - } - } - set { - self = .unsecured(value: newValue) - } - } -} -#endif // DEBUG diff --git a/apple/Tests/TestCases/Profile/Factor/PassphraseFactorSourceTests.swift b/apple/Tests/TestCases/Profile/Factor/PassphraseFactorSourceTests.swift new file mode 100644 index 000000000..c50a8f8d5 --- /dev/null +++ b/apple/Tests/TestCases/Profile/Factor/PassphraseFactorSourceTests.swift @@ -0,0 +1,80 @@ +// +// PassphraseFactorSourceTests.swift +// +// +// Created by Michael Bakogiannis on 2024-10-14. +// + +import CustomDump +import Foundation +import Sargon +import SargonUniFFI +import XCTest + +final class PassphraseFactorSourceTests: SpecificFactorSourceTest { + func test_id_of_passphrase() { + eachSample { sut in + XCTAssertEqual(sut.id.description, FactorSourceID.hash(value: sut.id).description) + } + } + + func test_new() { + XCTAssertEqual( + SUT( + mnemonicWithPassphrase: .init( + mnemonic: .samplePassphrase, + passphrase: "" + ) + ).id, + SUT.sample.id + ) + } + + func test_factor_source_id_is_id() { + eachSample { sut in + XCTAssertEqual(sut.id.asGeneral, sut.factorSourceID) + } + } + + func test_kind() { + eachSample { sut in + XCTAssertEqual(sut.factorSourceKind, .passphrase) + } + } + + func test_as() { + eachSample { sut in + XCTAssertEqual(sut.asGeneral.asPassphrase, sut) + } + } + + + func test_other_wrong() { + XCTAssertNil(SUT.extract(from: DeviceFactorSource.sample)) + } + + func test_as_factor_source_to_string() { + eachSample { sut in + XCTAssertEqual(sut.asGeneral.id.description, sut.id.description) + } + } + + func test_as_general() { + eachSample { sut in + XCTAssertEqual(sut.asGeneral, FactorSource.passphrase(value: sut)) + } + } + + func test_source_that_supports_babylon() { + let sut = SUT.sample + XCTAssertTrue(sut.supportsBabylon) + XCTAssertFalse(sut.supportsOlympia) + } + + + func test_extract_wrong_throws() throws { + try eachSample { sut in + XCTAssertThrowsError(try sut.asGeneral.extract(as: DeviceFactorSource.self)) + } + } +} diff --git a/apple/Tests/Utils/EntityProtocolTest.swift b/apple/Tests/Utils/EntityProtocolTest.swift index 807ac041d..3d1b7a90d 100644 --- a/apple/Tests/Utils/EntityProtocolTest.swift +++ b/apple/Tests/Utils/EntityProtocolTest.swift @@ -69,6 +69,8 @@ class EntityBaseTest: Test { case .ed25519: break // good case .secp256k1: XCTFail("Wrong key kind") } + case .securified(value: _): + XCTFail("Wrong security state") } } } diff --git a/crates/sargon/Cargo.toml b/crates/sargon/Cargo.toml index 6dd9c3d11..68819c3af 100644 --- a/crates/sargon/Cargo.toml +++ b/crates/sargon/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sargon" -version = "1.1.28" +version = "1.1.29" edition = "2021" build = "build.rs" @@ -49,6 +49,9 @@ serde_with = { git = "https://github.com/jonasbb/serde_with/", rev = "1e8e4e7539 # serde_repr = "0.1.17" serde_repr = { git = "https://github.com/dtolnay/serde-repr/", rev = "94cce18a51bc169869f2cdcea6549b3ed81b3b2e" } +# once_cell = "1.19.0" +once_cell = { git = "https://github.com/matklad/once_cell/", rev = "c48d3c2c01de926228aea2ac1d03672b4ce160c1" } + # thiserror = "1.0.50" thiserror = { git = "https://github.com/dtolnay/thiserror/", rev = "a7d220d7915fb888413aa7978efd70f7006bda9d" } diff --git a/crates/sargon/src/core/error/common_error.rs b/crates/sargon/src/core/error/common_error.rs index 444544c3b..1d9f215d5 100644 --- a/crates/sargon/src/core/error/common_error.rs +++ b/crates/sargon/src/core/error/common_error.rs @@ -656,6 +656,24 @@ pub enum CommonError { #[error("The network {network_id} does not exist in profile")] NoNetworkInProfile { network_id: NetworkID } = 10183, + + #[error("Empty FactorSources list")] + FactorSourcesOfKindEmptyFactors = 10184, + + #[error("Expected Passphrase factor source got something else")] + ExpectedPassphraseFactorSourceGotSomethingElse = 10185, + + #[error("Unknown persona.")] + UnknownPersona = 10186, + + #[error("Invalid security structure. Threshold ({}) cannot exceed threshold factors ({}).", threshold, factors)] + InvalidSecurityStructureThresholdExceedsFactors { + threshold: u8, + factors: u8, + } = 10187, + + #[error("Invalid security structure. A factor must not be present in both threshold and override list.")] + InvalidSecurityStructureFactorInBothThresholdAndOverride = 10188, } #[uniffi::export] diff --git a/crates/sargon/src/core/hash.rs b/crates/sargon/src/core/hash.rs index c171cfbcd..e45033ab9 100644 --- a/crates/sargon/src/core/hash.rs +++ b/crates/sargon/src/core/hash.rs @@ -120,6 +120,15 @@ impl HasSampleValues for Hash { } } +impl Hash { + pub fn sample_third() -> Self { + // "Just another hash".as_bytes() + "ba6af40c838cebdb29470bf6d2fae50a102197e368d3a62da9211a63e0043401" + .parse::() + .unwrap() + } +} + #[cfg(test)] mod tests { @@ -132,6 +141,7 @@ mod tests { fn equality() { assert_eq!(SUT::sample(), SUT::sample()); assert_eq!(SUT::sample_other(), SUT::sample_other()); + assert_eq!(SUT::sample_third(), SUT::sample_third()); } #[test] @@ -156,6 +166,11 @@ mod tests { hash_of("Radix... just imagine".as_bytes()).to_string(), "679dfbbda16d3f9669da8552ab6594d2b0446d03d96c076a0ec9dc550ea41fad" ); + + assert_eq!( + hash_of("Just another hash".as_bytes()).to_string(), + "ba6af40c838cebdb29470bf6d2fae50a102197e368d3a62da9211a63e0043401" + ); } #[test] diff --git a/crates/sargon/src/core/types/collections/identified_vec_of/identified_vec_of_query.rs b/crates/sargon/src/core/types/collections/identified_vec_of/identified_vec_of_query.rs index 6654522d5..005cb8374 100644 --- a/crates/sargon/src/core/types/collections/identified_vec_of/identified_vec_of_query.rs +++ b/crates/sargon/src/core/types/collections/identified_vec_of/identified_vec_of_query.rs @@ -22,7 +22,7 @@ impl IdentifiedVecOf { /// Check if the `item` exists in this map by calculating the ID of the item /// and checking if any other item with the same ID exists. pub fn contains_by_id(&self, item: &V) -> bool { - self.contains_id(&item.id()) + self.contains_id(item.id()) } /// Return `true`` if an item with `id` exists in the collection. diff --git a/crates/sargon/src/core/types/collections/just.rs b/crates/sargon/src/core/types/collections/just.rs new file mode 100644 index 000000000..0878e541e --- /dev/null +++ b/crates/sargon/src/core/types/collections/just.rs @@ -0,0 +1,74 @@ +use crate::prelude::*; + +pub trait Just { + fn just(item: Item) -> Self; +} +impl Just for IndexSet { + fn just(item: T) -> Self { + 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]) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_index_set() { + assert_eq!( + IndexSet::just(FactorSourceKind::Device), + IndexSet::::from_iter([FactorSourceKind::Device]) + ) + } + + #[test] + fn test_hash_set() { + assert_eq!( + HashSet::just(FactorSourceKind::Device), + HashSet::::from_iter([FactorSourceKind::Device]) + ) + } + + #[test] + fn test_index_map() { + assert_eq!( + IndexMap::just(( + FactorSourceKind::Device, + FactorSource::sample_device() + )), + IndexMap::::from_iter([( + FactorSourceKind::Device, + FactorSource::sample_device() + )]) + ) + } + + #[test] + fn test_hash_map() { + assert_eq!( + HashMap::just(( + FactorSourceKind::Device, + FactorSource::sample_device() + )), + HashMap::::from_iter([( + FactorSourceKind::Device, + FactorSource::sample_device() + )]) + ) + } +} diff --git a/crates/sargon/src/core/types/collections/mod.rs b/crates/sargon/src/core/types/collections/mod.rs index 0d78b0505..1dd0ce650 100644 --- a/crates/sargon/src/core/types/collections/mod.rs +++ b/crates/sargon/src/core/types/collections/mod.rs @@ -1,9 +1,11 @@ mod identified_vec_of; +mod just; #[cfg(test)] mod user; pub use identified_vec_of::*; +pub use just::*; #[cfg(test)] pub(super) use user::*; diff --git a/crates/sargon/src/core/types/decimal192.rs b/crates/sargon/src/core/types/decimal192.rs index 4e51b6063..e0359f714 100644 --- a/crates/sargon/src/core/types/decimal192.rs +++ b/crates/sargon/src/core/types/decimal192.rs @@ -1041,12 +1041,10 @@ mod test_decimal { )] #[test] fn engineering_for_abs_less_than_1_fails_pos() { - _ = SUT::try_from(0.9f32) - .unwrap() - .formatted_engineering_notation( - LocaleConfig::swedish_sweden(), - None, - ); + _ = SUT::from(0.9f32).formatted_engineering_notation( + LocaleConfig::swedish_sweden(), + None, + ); } #[should_panic( @@ -1054,12 +1052,10 @@ mod test_decimal { )] #[test] fn engineering_for_abs_less_than_1_fails_neg() { - _ = SUT::try_from(-0.9f32) - .unwrap() - .formatted_engineering_notation( - LocaleConfig::swedish_sweden(), - None, - ); + _ = SUT::from(-0.9f32).formatted_engineering_notation( + LocaleConfig::swedish_sweden(), + None, + ); } #[test] diff --git a/crates/sargon/src/core/types/decimal192_uniffi_fn.rs b/crates/sargon/src/core/types/decimal192_uniffi_fn.rs index 996385709..bd5ee6bec 100644 --- a/crates/sargon/src/core/types/decimal192_uniffi_fn.rs +++ b/crates/sargon/src/core/types/decimal192_uniffi_fn.rs @@ -476,10 +476,10 @@ mod uniffi_tests { let sut = new_decimal_from_f32(f); assert_eq!(sut.to_string(), "208050.17"); assert_eq!( - SUT::try_from(f32::MAX).unwrap().to_string(), + SUT::from(f32::MAX).to_string(), "340282350000000000000000000000000000000" ); - assert_eq!(SUT::try_from(f32::MIN_POSITIVE).unwrap().to_string(), "0"); + assert_eq!(SUT::from(f32::MIN_POSITIVE).to_string(), "0"); } #[test] diff --git a/crates/sargon/src/core/utils/image_url_utils.rs b/crates/sargon/src/core/utils/image_url_utils.rs index 48ec9a264..ff6f0af00 100644 --- a/crates/sargon/src/core/utils/image_url_utils.rs +++ b/crates/sargon/src/core/utils/image_url_utils.rs @@ -68,7 +68,7 @@ mod tests { fn test_is_vector_image_invalid_url() { let url = "invalid"; - assert_eq!(is_vector_image(url, VectorImageType::sample()), false); + assert!(!is_vector_image(url, VectorImageType::sample())); } #[test] diff --git a/crates/sargon/src/core/utils/image_url_utils_uniffi_fn.rs b/crates/sargon/src/core/utils/image_url_utils_uniffi_fn.rs index 722938cd1..2515a280d 100644 --- a/crates/sargon/src/core/utils/image_url_utils_uniffi_fn.rs +++ b/crates/sargon/src/core/utils/image_url_utils_uniffi_fn.rs @@ -31,7 +31,7 @@ mod tests { assert_eq!( is_vector_image(url, image_type), - image_url_utils_is_vector_image(&url, image_type) + image_url_utils_is_vector_image(url, image_type) ) } diff --git a/crates/sargon/src/gateway_api/models/logic/response/transaction/status/transaction_status.rs b/crates/sargon/src/gateway_api/models/logic/response/transaction/status/transaction_status.rs index bd649adda..177a4b319 100644 --- a/crates/sargon/src/gateway_api/models/logic/response/transaction/status/transaction_status.rs +++ b/crates/sargon/src/gateway_api/models/logic/response/transaction/status/transaction_status.rs @@ -15,7 +15,7 @@ mod tests { ))) .unwrap(); assert_eq!( - pending.known_payloads.get(0).unwrap().payload_status, + pending.known_payloads.first().unwrap().payload_status, Some(TransactionStatusResponsePayloadStatus::Pending) ); @@ -27,7 +27,7 @@ mod tests { assert_eq!( committed_success .known_payloads - .get(0) + .first() .unwrap() .payload_status, Some(TransactionStatusResponsePayloadStatus::CommittedSuccess) diff --git a/crates/sargon/src/hierarchical_deterministic/bip39/mnemonic.rs b/crates/sargon/src/hierarchical_deterministic/bip39/mnemonic.rs index 45ad8c518..873b1ad1d 100644 --- a/crates/sargon/src/hierarchical_deterministic/bip39/mnemonic.rs +++ b/crates/sargon/src/hierarchical_deterministic/bip39/mnemonic.rs @@ -324,6 +324,31 @@ impl Mnemonic { pub(crate) fn sample_arculus_other() -> Self { Self::from_phrase("arch card helmet sign source sample other arch card sample other arch card sample other arch card sample other arch card sample other lock").expect("Valid mnemonic") } + + #[allow(dead_code)] + /// Alternative valid mnemonics, with last (checksum) words changed only are: + /// * brass + /// * crater + /// * embrace + /// * invest + /// * music + /// * project + /// * uphold + pub(crate) fn sample_passphrase() -> Self { + Self::from_phrase("pass phrase sign source sample pass phrase sign source sample pass phrase sign source sample pass phrase sign source sample pass phrase sample soon").expect("Valid mnemonic") + } + #[allow(dead_code)] + /// Alternative valid mnemonics, with last (checksum) words changed only are: + /// * animal + /// * collect + /// * dragon + /// * gold + /// * once + /// * ripple + /// * summer + pub(crate) fn sample_passphrase_other() -> Self { + Self::from_phrase("pass phrase sign source sample other pass phrase sign source sample other pass phrase sign source sample other pass phrase source sample other usual").expect("Valid mnemonic") + } } #[cfg(test)] diff --git a/crates/sargon/src/hierarchical_deterministic/bip39/mnemonic_uniffi_fn.rs b/crates/sargon/src/hierarchical_deterministic/bip39/mnemonic_uniffi_fn.rs index 8c8461d9a..9d9b3b0c0 100644 --- a/crates/sargon/src/hierarchical_deterministic/bip39/mnemonic_uniffi_fn.rs +++ b/crates/sargon/src/hierarchical_deterministic/bip39/mnemonic_uniffi_fn.rs @@ -103,6 +103,16 @@ pub fn new_mnemonic_sample_arculus_other() -> Mnemonic { Mnemonic::sample_arculus_other() } +#[uniffi::export] +pub fn new_mnemonic_sample_passphrase() -> Mnemonic { + Mnemonic::sample_passphrase() +} + +#[uniffi::export] +pub fn new_mnemonic_sample_passphrase_other() -> Mnemonic { + Mnemonic::sample_passphrase_other() +} + #[cfg(test)] mod uniffi_tests { use super::*; @@ -154,9 +164,11 @@ mod uniffi_tests { new_mnemonic_sample_security_questions_other(), new_mnemonic_sample_arculus(), new_mnemonic_sample_arculus_other(), + new_mnemonic_sample_passphrase(), + new_mnemonic_sample_passphrase_other() ]) .len(), - 12 + 14 ); } diff --git a/crates/sargon/src/hierarchical_deterministic/derivation/mnemonic_with_passphrase.rs b/crates/sargon/src/hierarchical_deterministic/derivation/mnemonic_with_passphrase.rs index 3ba331062..4166b9b0c 100644 --- a/crates/sargon/src/hierarchical_deterministic/derivation/mnemonic_with_passphrase.rs +++ b/crates/sargon/src/hierarchical_deterministic/derivation/mnemonic_with_passphrase.rs @@ -189,6 +189,34 @@ impl MnemonicWithPassphrase { BIP39Passphrase::new("Leonidas"), ) } + + pub(crate) fn sample_security_questions() -> Self { + Self::with_passphrase( + Mnemonic::sample_security_questions(), + BIP39Passphrase::default(), + ) + } + + pub(crate) fn sample_security_questions_other() -> Self { + Self::with_passphrase( + Mnemonic::sample_security_questions_other(), + BIP39Passphrase::new("Questions?"), + ) + } + + pub(crate) fn sample_passphrase() -> Self { + Self::with_passphrase( + Mnemonic::sample_passphrase(), + BIP39Passphrase::default(), + ) + } + + pub(crate) fn sample_passphrase_other() -> Self { + Self::with_passphrase( + Mnemonic::sample_security_questions_other(), + BIP39Passphrase::new("Pass phrase"), + ) + } } impl HasSampleValues for MnemonicWithPassphrase { diff --git a/crates/sargon/src/lib.rs b/crates/sargon/src/lib.rs index 0a12f2611..ceaa4c05d 100644 --- a/crates/sargon/src/lib.rs +++ b/crates/sargon/src/lib.rs @@ -3,6 +3,7 @@ #![feature(core_intrinsics)] #![allow(unused_imports)] #![allow(internal_features)] +#![feature(iter_repeat_n)] mod core; mod gateway_api; @@ -10,6 +11,7 @@ mod hierarchical_deterministic; mod home_cards; mod profile; mod radix_connect; +mod signing; mod system; mod types; mod wrapped_radix_engine_toolkit; @@ -22,7 +24,9 @@ pub mod prelude { pub use crate::home_cards::*; pub use crate::profile::*; pub use crate::radix_connect::*; + pub use crate::signing::*; pub use crate::system::*; + pub use crate::types::*; pub use crate::wrapped_radix_engine_toolkit::*; pub(crate) use radix_rust::prelude::{ @@ -34,6 +38,7 @@ pub mod prelude { pub(crate) use iso8601_timestamp::Timestamp; pub(crate) use itertools::Itertools; pub(crate) use log::{debug, error, info, trace, warn}; + pub(crate) use once_cell::sync::Lazy; pub(crate) use serde::{ de, ser::SerializeStruct, Deserialize, Deserializer, Serialize, Serializer, @@ -44,6 +49,7 @@ pub mod prelude { pub(crate) use zeroize::{Zeroize, ZeroizeOnDrop}; pub use radix_common::math::traits::CheckedMul as ScryptoCheckedMul; + pub(crate) use std::cell::RefCell; pub(crate) use std::cmp::Ordering; pub(crate) use std::collections::BTreeMap; pub(crate) use std::fmt::{Debug, Display, Formatter}; diff --git a/crates/sargon/src/profile/logic/account/mod.rs b/crates/sargon/src/profile/logic/account/mod.rs index 909c4f742..41423635d 100644 --- a/crates/sargon/src/profile/logic/account/mod.rs +++ b/crates/sargon/src/profile/logic/account/mod.rs @@ -1,9 +1,11 @@ mod accounts_visibility; mod create_account; mod query_accounts; +mod query_personas; mod query_security_structures; pub use accounts_visibility::*; pub use create_account::*; pub use query_accounts::*; +pub use query_personas::*; pub use query_security_structures::*; diff --git a/crates/sargon/src/profile/logic/account/query_personas.rs b/crates/sargon/src/profile/logic/account/query_personas.rs new file mode 100644 index 000000000..46be06263 --- /dev/null +++ b/crates/sargon/src/profile/logic/account/query_personas.rs @@ -0,0 +1,58 @@ +use crate::prelude::*; + +impl Profile { + /// Returns the non-hidden personas on the current network, empty if no personas + /// on the network + pub fn personas_on_current_network(&self) -> Result { + self.current_network().map(|n| n.personas.non_hidden()) + } + + /// Looks up the persona by identity address, returns Err if the persona is + /// unknown, will return a hidden persona if queried for. + pub fn persona_by_address( + &self, + address: IdentityAddress, + ) -> Result { + for network in self.networks.iter() { + if let Some(persona) = network.personas.get_id(address) { + return Ok(persona.clone()); + } + } + Err(CommonError::UnknownPersona) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = Profile; + + #[test] + fn test_personas_on_current_network() { + let sut = SUT::sample(); + assert_eq!( + sut.personas_on_current_network().unwrap(), + Personas::sample_mainnet() + ); + } + + #[test] + fn test_personas_on_current_network_stokenet() { + let sut = SUT::sample_other(); + assert_eq!( + sut.personas_on_current_network().unwrap(), + Personas::just(Persona::sample_stokenet_leia_skywalker()) // Hermione is hidden + ); + } + + #[test] + fn test_persona_by_address() { + let sut = SUT::sample(); + assert_eq!( + sut.persona_by_address(Persona::sample_mainnet().address), + Ok(Persona::sample_mainnet()) + ); + } +} diff --git a/crates/sargon/src/profile/logic/profile_network/profile_network_details.rs b/crates/sargon/src/profile/logic/profile_network/profile_network_details.rs index 43d5f7900..c74bbab18 100644 --- a/crates/sargon/src/profile/logic/profile_network/profile_network_details.rs +++ b/crates/sargon/src/profile/logic/profile_network/profile_network_details.rs @@ -122,6 +122,7 @@ impl AuthorizedPersonaSimple { EntitySecurityState::Unsecured { value: uec } => { uec.authentication_signing.is_some() } + _ => false, // TODO change that }; Ok(AuthorizedPersonaDetailed::new( persona.address, diff --git a/crates/sargon/src/profile/logic/profile_next_derivation.rs b/crates/sargon/src/profile/logic/profile_next_derivation.rs index 36434f3cc..9adc5e8fb 100644 --- a/crates/sargon/src/profile/logic/profile_next_derivation.rs +++ b/crates/sargon/src/profile/logic/profile_next_derivation.rs @@ -86,6 +86,9 @@ impl Profile { value.transaction_signing.factor_source_id == factor_source_id } + EntitySecurityState::Securified { value: _ } => { + panic!("Not implemented yet") + } }) .collect_vec() .len() @@ -147,14 +150,13 @@ impl Profile { .collect_vec(); main_factors.iter().for_each(|id| { - _ = profile.factor_sources.update_with(id, |f| { - _ = f - .as_device_mut() + profile.factor_sources.update_with(id, |f| { + f.as_device_mut() .unwrap() .common .flags - .remove_id(&FactorSourceFlag::Main) - }) + .remove_id(&FactorSourceFlag::Main); + }); }); profile @@ -264,7 +266,7 @@ mod tests { )] fn bdfs_fail_for_invalid_profile_without_device_factor_source() { let profile = Profile::sample_no_device_factor_source(); - _ = profile.bdfs(); + profile.bdfs(); } #[test] @@ -273,7 +275,7 @@ mod tests { )] fn bdfs_fail_for_invalid_profile_without_babylon_device_factor_source() { let profile = Profile::sample_no_babylon_device_factor_source(); - _ = profile.bdfs(); + profile.bdfs(); } #[test] diff --git a/crates/sargon/src/profile/mfa/mfa_factor_sources/mod.rs b/crates/sargon/src/profile/mfa/mfa_factor_sources/mod.rs index 3d624cfc4..abdda9268 100644 --- a/crates/sargon/src/profile/mfa/mfa_factor_sources/mod.rs +++ b/crates/sargon/src/profile/mfa/mfa_factor_sources/mod.rs @@ -1,9 +1,11 @@ mod arculus_card_factor_source; mod off_device_mnemonic_factor_source; +mod passphrase_factor_source; mod security_questions_factor_source; mod trusted_contact_factor_source; pub use arculus_card_factor_source::*; pub use off_device_mnemonic_factor_source::*; +pub use passphrase_factor_source::*; pub use security_questions_factor_source::*; pub use trusted_contact_factor_source::*; diff --git a/crates/sargon/src/profile/mfa/mfa_factor_sources/passphrase_factor_source/mod.rs b/crates/sargon/src/profile/mfa/mfa_factor_sources/passphrase_factor_source/mod.rs new file mode 100644 index 000000000..2a7f6a680 --- /dev/null +++ b/crates/sargon/src/profile/mfa/mfa_factor_sources/passphrase_factor_source/mod.rs @@ -0,0 +1,5 @@ +mod passphrase_factor_source; +mod passphrase_factor_source_uniffi_fn; + +pub use passphrase_factor_source::*; +pub use passphrase_factor_source_uniffi_fn::*; diff --git a/crates/sargon/src/profile/mfa/mfa_factor_sources/passphrase_factor_source/passphrase_factor_source.rs b/crates/sargon/src/profile/mfa/mfa_factor_sources/passphrase_factor_source/passphrase_factor_source.rs new file mode 100644 index 000000000..4b101838b --- /dev/null +++ b/crates/sargon/src/profile/mfa/mfa_factor_sources/passphrase_factor_source/passphrase_factor_source.rs @@ -0,0 +1,178 @@ +use crate::prelude::*; + +/// NOT IMPLEMENTED NOR USED YET +/// +/// A passphrase based FactorSource is essentially a Input Key Material based Mnemonic, +/// user needs to input the passphrase - key material - every time they use this factor source +#[derive( + Serialize, + Deserialize, + Debug, + Clone, + PartialEq, + Eq, + Hash, + derive_more::Display, + uniffi::Record, +)] +#[serde(rename_all = "camelCase")] +#[display("{id}")] +pub struct PassphraseFactorSource { + /// Unique and stable identifier of this factor source, stemming from the + /// hash of a special child key of the HD root of the mnemonic. + pub id: FactorSourceIDFromHash, + + /// Common properties shared between FactorSources of different kinds, + /// describing its state, when added, and supported cryptographic parameters. + pub common: FactorSourceCommon, +} + +impl PassphraseFactorSource { + /// Instantiates a new `PassphraseFactorSource` + pub fn new(id: FactorSourceIDFromHash) -> Self { + Self { + id, + common: FactorSourceCommon::new_bdfs(false), + } + } +} + +impl From for FactorSource { + fn from(value: PassphraseFactorSource) -> Self { + FactorSource::Passphrase { value } + } +} + +fn new_passphrase_with_mwp( + mwp: MnemonicWithPassphrase, +) -> PassphraseFactorSource { + let id = FactorSourceIDFromHash::new_for_passphrase(&mwp); + let mut source = PassphraseFactorSource::new(id); + source.common.last_used_on = Timestamp::sample(); + source.common.added_on = Timestamp::sample(); + source +} + +impl HasSampleValues for PassphraseFactorSource { + fn sample() -> Self { + new_passphrase_with_mwp(MnemonicWithPassphrase::sample_passphrase()) + } + + fn sample_other() -> Self { + new_passphrase_with_mwp( + MnemonicWithPassphrase::sample_passphrase_other(), + ) + } +} + +impl TryFrom for PassphraseFactorSource { + type Error = CommonError; + + fn try_from(value: FactorSource) -> Result { + match value { + FactorSource::Passphrase { value } => Ok(value), + _ => { + Err(Self::Error::ExpectedPassphraseFactorSourceGotSomethingElse) + } + } + } +} + +impl IsFactorSource for PassphraseFactorSource { + fn kind() -> FactorSourceKind { + FactorSourceKind::Passphrase + } +} +impl BaseIsFactorSource for PassphraseFactorSource { + fn factor_source_kind(&self) -> FactorSourceKind { + self.id.kind + } + + fn factor_source_id(&self) -> FactorSourceID { + self.clone().id.into() + } + + fn common_properties(&self) -> FactorSourceCommon { + self.common.clone() + } + + fn set_common_properties(&mut self, updated: FactorSourceCommon) { + self.common = updated + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = PassphraseFactorSource; + + #[test] + fn equality() { + assert_eq!(SUT::sample(), SUT::sample()); + assert_eq!(SUT::sample_other(), SUT::sample_other()); + } + + #[test] + fn inequality() { + assert_ne!(SUT::sample(), SUT::sample_other()); + } + + #[test] + fn json_roundtrip() { + let model = SUT::sample(); + assert_eq_after_json_roundtrip( + &model, + r#" + { + "id": { + "kind": "passphrase", + "body": "181ab662e19fac3ad9f08d5c673b286d4a5ed9cd3762356dc9831dc42427c1b9" + }, + "common": { + "addedOn": "2023-09-11T16:05:56.000Z", + "cryptoParameters": { + "supportedCurves": ["curve25519"], + "supportedDerivationPathSchemes": ["cap26"] + }, + "flags": [], + "lastUsedOn": "2023-09-11T16:05:56.000Z" + } + } + "#, + ); + } + + #[test] + fn from_factor_source() { + let sut = SUT::sample(); + let factor_source: FactorSource = sut.clone().into(); + assert_eq!(SUT::try_from(factor_source), Ok(sut)); + } + + #[test] + fn kind() { + assert_eq!(SUT::kind(), FactorSourceKind::Passphrase); + } + + #[test] + fn from_factor_source_invalid_got_device() { + let wrong = DeviceFactorSource::sample(); + let factor_source: FactorSource = wrong.clone().into(); + assert_eq!( + SUT::try_from(factor_source), + Err(CommonError::ExpectedPassphraseFactorSourceGotSomethingElse) + ); + } + + #[test] + fn factor_source_id() { + assert_eq!(SUT::sample().factor_source_id(), SUT::sample().id.into()); + } + + #[test] + fn factor_source_kind() { + assert_eq!(SUT::sample().factor_source_kind(), SUT::sample().id.kind); + } +} diff --git a/crates/sargon/src/profile/mfa/mfa_factor_sources/passphrase_factor_source/passphrase_factor_source_uniffi_fn.rs b/crates/sargon/src/profile/mfa/mfa_factor_sources/passphrase_factor_source/passphrase_factor_source_uniffi_fn.rs new file mode 100644 index 000000000..d8fb5f798 --- /dev/null +++ b/crates/sargon/src/profile/mfa/mfa_factor_sources/passphrase_factor_source/passphrase_factor_source_uniffi_fn.rs @@ -0,0 +1,53 @@ +use crate::prelude::*; + +#[uniffi::export] +pub fn new_passphrase_factor_source_sample() -> PassphraseFactorSource { + PassphraseFactorSource::sample() +} + +#[uniffi::export] +pub fn new_passphrase_factor_source_sample_other() -> PassphraseFactorSource { + PassphraseFactorSource::sample_other() +} + +#[uniffi::export] +fn new_passphrase_factor_source_from_mnemonic_with_passphrase( + mwp: MnemonicWithPassphrase, +) -> PassphraseFactorSource { + let id = FactorSourceIDFromHash::new_for_passphrase(&mwp); + PassphraseFactorSource::new(id) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = PassphraseFactorSource; + + #[test] + fn hash_of_samples() { + assert_eq!( + HashSet::::from_iter([ + new_passphrase_factor_source_sample(), + new_passphrase_factor_source_sample_other(), + // duplicates should get removed + new_passphrase_factor_source_sample(), + new_passphrase_factor_source_sample_other(), + ]) + .len(), + 2 + ); + } + + #[test] + fn test_new_arculus_card_factor_source_from_mnemonic_with_passphrase() { + assert_eq!( + new_passphrase_factor_source_from_mnemonic_with_passphrase( + MnemonicWithPassphrase::sample_passphrase(), + ) + .factor_source_id(), + SUT::sample().factor_source_id() + ); + } +} diff --git a/crates/sargon/src/profile/mfa/security_structures/decl_security_structure_of.rs b/crates/sargon/src/profile/mfa/security_structures/decl_security_structure_of.rs index 40252dffc..f84faacbd 100644 --- a/crates/sargon/src/profile/mfa/security_structures/decl_security_structure_of.rs +++ b/crates/sargon/src/profile/mfa/security_structures/decl_security_structure_of.rs @@ -79,18 +79,38 @@ macro_rules! decl_role_with_factors { } impl [< $role RoleWith $factor s >] { + // # Panics + /// Panics if threshold > threshold_factor.len() + /// + /// Panics if the same factor is present in both lists pub fn new( threshold_factors: impl IntoIterator, threshold: u8, override_factors: impl IntoIterator - ) -> Self { - let _self = Self { - threshold_factors: threshold_factors.into_iter().collect(), + ) -> Result { + let threshold_factors = threshold_factors.into_iter().collect_vec(); + + if threshold_factors.len() < threshold as usize { + return Err(CommonError::InvalidSecurityStructureThresholdExceedsFactors { + threshold, + factors: threshold_factors.len() as u8 + }) + } + + let override_factors = override_factors.into_iter().collect_vec(); + + if !HashSet::<$factor>::from_iter(threshold_factors.clone()) + .intersection(&HashSet::<$factor>::from_iter(override_factors.clone())) + .collect_vec() + .is_empty() { + return Err(CommonError::InvalidSecurityStructureFactorInBothThresholdAndOverride) + } + + Ok(Self { + threshold_factors, threshold, - override_factors: override_factors.into_iter().collect(), - }; - assert!(_self.threshold_factors.len() >= _self.threshold as usize); - _self + override_factors, + }) } pub fn all_factors(&self) -> HashSet<&$factor> { diff --git a/crates/sargon/src/profile/mfa/security_structures/security_structure_of_factor_source_ids.rs b/crates/sargon/src/profile/mfa/security_structures/security_structure_of_factor_source_ids.rs index dbbe83c12..fffe0ce42 100644 --- a/crates/sargon/src/profile/mfa/security_structures/security_structure_of_factor_source_ids.rs +++ b/crates/sargon/src/profile/mfa/security_structures/security_structure_of_factor_source_ids.rs @@ -22,6 +22,7 @@ impl From for PrimaryRoleWithFactorSourceIDs { value.threshold, value.override_factors.iter().map(|x| x.factor_source_id()), ) + .expect("PrimaryRoleWithFactorSources has already been validated.") } } @@ -32,6 +33,7 @@ impl From for RecoveryRoleWithFactorSourceIDs { value.threshold, value.override_factors.iter().map(|x| x.factor_source_id()), ) + .expect("RecoveryRoleWithFactorSources has already been validated.") } } @@ -44,6 +46,7 @@ impl From value.threshold, value.override_factors.iter().map(|x| x.factor_source_id()), ) + .expect("ConfirmationRoleWithFactorSources has already been validated.") } } diff --git a/crates/sargon/src/profile/mfa/security_structures/security_structure_of_factor_sources.rs b/crates/sargon/src/profile/mfa/security_structures/security_structure_of_factor_sources.rs index 01db66011..3610b59af 100644 --- a/crates/sargon/src/profile/mfa/security_structures/security_structure_of_factor_sources.rs +++ b/crates/sargon/src/profile/mfa/security_structures/security_structure_of_factor_sources.rs @@ -50,7 +50,7 @@ fn factors_from( .ok_or(CommonError::ProfileDoesNotContainFactorSourceWithID { bad_value: *id, }) - .map(|x| x.clone()) + .cloned() }) .collect::>() } @@ -69,11 +69,7 @@ impl TryFrom<(&PrimaryRoleWithFactorSourceIDs, &FactorSources)> let override_factors = factors_from(&id_level.override_factors, factor_sources)?; - Ok(Self::new( - threshold_factors, - id_level.threshold, - override_factors, - )) + Self::new(threshold_factors, id_level.threshold, override_factors) } } impl TryFrom<(&RecoveryRoleWithFactorSourceIDs, &FactorSources)> @@ -90,11 +86,7 @@ impl TryFrom<(&RecoveryRoleWithFactorSourceIDs, &FactorSources)> let override_factors = factors_from(&id_level.override_factors, factor_sources)?; - Ok(Self::new( - threshold_factors, - id_level.threshold, - override_factors, - )) + Self::new(threshold_factors, id_level.threshold, override_factors) } } impl TryFrom<(&ConfirmationRoleWithFactorSourceIDs, &FactorSources)> @@ -111,11 +103,7 @@ impl TryFrom<(&ConfirmationRoleWithFactorSourceIDs, &FactorSources)> let override_factors = factors_from(&id_level.override_factors, factor_sources)?; - Ok(Self::new( - threshold_factors, - id_level.threshold, - override_factors, - )) + Self::new(threshold_factors, id_level.threshold, override_factors) } } @@ -177,6 +165,7 @@ impl HasSampleValues for PrimaryRoleWithFactorSources { 2, [FactorSource::sample_ledger()], ) + .unwrap() } fn sample_other() -> Self { Self::new( @@ -188,6 +177,7 @@ impl HasSampleValues for PrimaryRoleWithFactorSources { 2, [FactorSource::sample_ledger_other()], ) + .unwrap() } } @@ -202,6 +192,7 @@ impl HasSampleValues for RecoveryRoleWithFactorSources { 2, [FactorSource::sample_ledger()], ) + .unwrap() } fn sample_other() -> Self { Self::new( @@ -213,6 +204,7 @@ impl HasSampleValues for RecoveryRoleWithFactorSources { 2, [FactorSource::sample_ledger_other()], ) + .unwrap() } } @@ -226,6 +218,7 @@ impl HasSampleValues for ConfirmationRoleWithFactorSources { FactorSource::sample_ledger(), ], ) + .unwrap() } fn sample_other() -> Self { Self::new( @@ -236,6 +229,7 @@ impl HasSampleValues for ConfirmationRoleWithFactorSources { FactorSource::sample_ledger_other(), ], ) + .unwrap() } } diff --git a/crates/sargon/src/profile/supporting_types/account_or_persona.rs b/crates/sargon/src/profile/supporting_types/account_or_persona.rs index 9e81143cd..b83b44205 100644 --- a/crates/sargon/src/profile/supporting_types/account_or_persona.rs +++ b/crates/sargon/src/profile/supporting_types/account_or_persona.rs @@ -2,7 +2,15 @@ use crate::prelude::*; /// Either an `Account` or a `Persona`. #[derive( - Serialize, Deserialize, Clone, Debug, PartialEq, Hash, Eq, uniffi::Enum, + Serialize, + Deserialize, + Clone, + Debug, + PartialEq, + Hash, + Eq, + EnumAsInner, + uniffi::Enum, )] pub enum AccountOrPersona { /// An `Account` @@ -100,6 +108,28 @@ impl AccountOrPersona { pub(crate) fn sample_stokenet_third() -> Self { Self::from(Account::sample_stokenet_third()) } + + pub fn entity_security_state(&self) -> EntitySecurityState { + match self { + AccountOrPersona::AccountEntity(account) => { + account.security_state.clone() + } + AccountOrPersona::PersonaEntity(persona) => { + persona.security_state.clone() + } + } + } + + pub fn address(&self) -> AddressOfAccountOrPersona { + match self { + AccountOrPersona::AccountEntity(account) => { + AddressOfAccountOrPersona::Account(account.address) + } + AccountOrPersona::PersonaEntity(persona) => { + AddressOfAccountOrPersona::Identity(persona.address) + } + } + } } #[cfg(test)] diff --git a/crates/sargon/src/profile/v100/entity/account/account.rs b/crates/sargon/src/profile/v100/entity/account/account.rs index 6bc413533..8017033d3 100644 --- a/crates/sargon/src/profile/v100/entity/account/account.rs +++ b/crates/sargon/src/profile/v100/entity/account/account.rs @@ -119,27 +119,6 @@ impl Identifiable for Account { } } -impl PartialOrd for Account { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for Account { - fn cmp(&self, other: &Self) -> Ordering { - match (&self.security_state, &other.security_state) { - ( - EntitySecurityState::Unsecured { value: l }, - EntitySecurityState::Unsecured { value: r }, - ) => l - .transaction_signing - .derivation_path() - .last_component() - .cmp(r.transaction_signing.derivation_path().last_component()), - } - } -} - impl HasSampleValues for Account { /// A sample used to facilitate unit tests. fn sample() -> Self { @@ -383,11 +362,6 @@ mod tests { ); } - #[test] - fn compare() { - assert!(SUT::sample_alice() < SUT::sample_bob()); - } - #[test] fn update() { let mut account = SUT::sample(); diff --git a/crates/sargon/src/profile/v100/entity/persona/persona.rs b/crates/sargon/src/profile/v100/entity/persona/persona.rs index 6f05b0c98..fb4462df3 100644 --- a/crates/sargon/src/profile/v100/entity/persona/persona.rs +++ b/crates/sargon/src/profile/v100/entity/persona/persona.rs @@ -372,27 +372,6 @@ impl Persona { } } -impl Ord for Persona { - fn cmp(&self, other: &Self) -> Ordering { - match (&self.security_state, &other.security_state) { - ( - EntitySecurityState::Unsecured { value: l }, - EntitySecurityState::Unsecured { value: r }, - ) => l - .transaction_signing - .derivation_path() - .last_component() - .cmp(r.transaction_signing.derivation_path().last_component()), - } - } -} - -impl PartialOrd for Persona { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - /// Add conformance to Identifiable in order to use `IdentifiedVecOf` impl Identifiable for Persona { type ID = IdentityAddress; @@ -450,11 +429,6 @@ mod tests { assert_eq!(SUT::sample().network_id(), NetworkID::Mainnet); } - #[test] - fn compare() { - assert!(SUT::sample_mainnet_other() > SUT::sample_mainnet()); - } - #[test] fn new_with_identity_and_name() { let identity_address: IdentityAddress = diff --git a/crates/sargon/src/profile/v100/entity_security_state/entity_security_state.rs b/crates/sargon/src/profile/v100/entity_security_state/entity_security_state.rs index 6c18426f6..59fc25e81 100644 --- a/crates/sargon/src/profile/v100/entity_security_state/entity_security_state.rs +++ b/crates/sargon/src/profile/v100/entity_security_state/entity_security_state.rs @@ -21,6 +21,11 @@ pub enum EntitySecurityState { #[serde(rename = "unsecuredEntityControl")] value: UnsecuredEntityControl, }, + /// The account is controlled by multi-factor + Securified { + #[serde(rename = "securedEntityControl")] + value: SecuredEntityControl, + }, } impl<'de> Deserialize<'de> for EntitySecurityState { @@ -51,6 +56,10 @@ impl Serialize for EntitySecurityState { state.serialize_field("discriminator", "unsecured")?; state.serialize_field("unsecuredEntityControl", value)?; } + EntitySecurityState::Securified { value } => { + state.serialize_field("discriminator", "securified")?; + state.serialize_field("securedEntityControl", value)?; + } } state.end() } @@ -62,6 +71,12 @@ impl From for EntitySecurityState { } } +impl From for EntitySecurityState { + fn from(value: SecuredEntityControl) -> Self { + Self::Securified { value } + } +} + impl HasSampleValues for EntitySecurityState { /// A sample used to facilitate unit tests. fn sample() -> Self { @@ -135,4 +150,153 @@ mod tests { "#, ); } + + #[test] + fn test() { + let model = EntitySecurityState::Securified { + value: SecuredEntityControl { + access_controller_address: AccessControllerAddress::sample(), + security_structure: SecurityStructureOfFactorInstances::new( + SecurityStructureID::sample(), + MatrixOfFactorInstances::new( + PrimaryRoleWithFactorInstances::new( + [FactorInstance::sample()], + 1, + [], + ) + .unwrap(), + RecoveryRoleWithFactorInstances::new( + [FactorInstance::new( + FactorSourceIDFromHash::sample_ledger().into(), + FactorInstanceBadge::sample(), + )], + 1, + [], + ) + .unwrap(), + ConfirmationRoleWithFactorInstances::new( + [FactorInstance::new( + FactorSourceIDFromHash::sample_passphrase() + .into(), + FactorInstanceBadge::sample(), + )], + 1, + [], + ) + .unwrap(), + ), + ), + }, + }; + + assert_eq_after_json_roundtrip( + &model, + r#" + { + "discriminator": "securified", + "securedEntityControl": { + "accessControllerAddress": "accesscontroller_rdx1c0duj4lq0dc3cpl8qd420fpn5eckh8ljeysvjm894lyl5ja5yq6y5a", + "securityStructure": { + "securityStructureId": "ffffffff-ffff-ffff-ffff-ffffffffffff", + "matrixOfFactors": { + "primaryRole": { + "thresholdFactors": [ + { + "factorSourceID": { + "discriminator": "fromHash", + "fromHash": { + "kind": "device", + "body": "f1a93d324dd0f2bff89963ab81ed6e0c2ee7e18c0827dc1d3576b2d9f26bbd0a" + } + }, + "badge": { + "discriminator": "virtualSource", + "virtualSource": { + "discriminator": "hierarchicalDeterministicPublicKey", + "hierarchicalDeterministicPublicKey": { + "publicKey": { + "curve": "curve25519", + "compressedData": "c05f9fa53f203a01cbe43e89086cae29f6c7cdd5a435daa9e52b69e656739b36" + }, + "derivationPath": { + "scheme": "cap26", + "path": "m/44H/1022H/1H/525H/1460H/0H" + } + } + } + } + } + ], + "threshold": 1, + "overrideFactors": [] + }, + "recoveryRole": { + "thresholdFactors": [ + { + "factorSourceID": { + "discriminator": "fromHash", + "fromHash": { + "kind": "ledgerHQHardwareWallet", + "body": "ab59987eedd181fe98e512c1ba0f5ff059f11b5c7c56f15614dcc9fe03fec58b" + } + }, + "badge": { + "discriminator": "virtualSource", + "virtualSource": { + "discriminator": "hierarchicalDeterministicPublicKey", + "hierarchicalDeterministicPublicKey": { + "publicKey": { + "curve": "curve25519", + "compressedData": "c05f9fa53f203a01cbe43e89086cae29f6c7cdd5a435daa9e52b69e656739b36" + }, + "derivationPath": { + "scheme": "cap26", + "path": "m/44H/1022H/1H/525H/1460H/0H" + } + } + } + } + } + ], + "threshold": 1, + "overrideFactors": [] + }, + "confirmationRole": { + "thresholdFactors": [ + { + "factorSourceID": { + "discriminator": "fromHash", + "fromHash": { + "kind": "passphrase", + "body": "181ab662e19fac3ad9f08d5c673b286d4a5ed9cd3762356dc9831dc42427c1b9" + } + }, + "badge": { + "discriminator": "virtualSource", + "virtualSource": { + "discriminator": "hierarchicalDeterministicPublicKey", + "hierarchicalDeterministicPublicKey": { + "publicKey": { + "curve": "curve25519", + "compressedData": "c05f9fa53f203a01cbe43e89086cae29f6c7cdd5a435daa9e52b69e656739b36" + }, + "derivationPath": { + "scheme": "cap26", + "path": "m/44H/1022H/1H/525H/1460H/0H" + } + } + } + } + } + ], + "threshold": 1, + "overrideFactors": [] + } + } + } + } + } + "#, + ); + } } diff --git a/crates/sargon/src/profile/v100/factors/factor_source.rs b/crates/sargon/src/profile/v100/factors/factor_source.rs index 48a76ac5d..de2c97616 100644 --- a/crates/sargon/src/profile/v100/factors/factor_source.rs +++ b/crates/sargon/src/profile/v100/factors/factor_source.rs @@ -49,6 +49,12 @@ pub enum FactorSource { #[display("TrustedContact({value})")] value: TrustedContactFactorSource, }, + + Passphrase { + #[serde(rename = "passphrase")] + #[display("Passphrase({value})")] + value: PassphraseFactorSource, + }, } impl BaseIsFactorSource for FactorSource { @@ -72,6 +78,9 @@ impl BaseIsFactorSource for FactorSource { FactorSource::TrustedContact { value } => { value.set_common_properties(updated) } + FactorSource::Passphrase { value } => { + value.set_common_properties(updated) + } } } @@ -87,6 +96,7 @@ impl BaseIsFactorSource for FactorSource { value.common_properties() } FactorSource::TrustedContact { value } => value.common_properties(), + FactorSource::Passphrase { value } => value.common_properties(), } } @@ -104,6 +114,7 @@ impl BaseIsFactorSource for FactorSource { FactorSource::TrustedContact { value } => { value.factor_source_kind() } + FactorSource::Passphrase { value } => value.factor_source_kind(), } } @@ -119,6 +130,7 @@ impl BaseIsFactorSource for FactorSource { value.factor_source_id() } FactorSource::TrustedContact { value } => value.factor_source_id(), + FactorSource::Passphrase { value } => value.factor_source_id(), } } } @@ -153,6 +165,29 @@ impl FactorSource { } } +impl PartialOrd for FactorSource { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} +impl Ord for FactorSource { + fn cmp(&self, other: &Self) -> Ordering { + match self.factor_source_kind().cmp(&other.factor_source_kind()) { + Ordering::Equal => {} + ord => return ord, + } + + let self_last_used = self.common_properties().last_used_on; + let other_last_used = &other.common_properties().last_used_on; + match self_last_used.cmp(other_last_used) { + Ordering::Equal => {} + ord => return ord, + } + + Ordering::Equal + } +} + impl<'de> Deserialize<'de> for FactorSource { #[cfg(not(tarpaulin_include))] // false negative fn deserialize>( @@ -208,6 +243,11 @@ impl Serialize for FactorSource { state.serialize_field(discriminator_key, discriminant)?; state.serialize_field(discriminant, value)?; } + FactorSource::Passphrase { value } => { + let discriminant = "passphrase"; + state.serialize_field(discriminator_key, discriminant)?; + state.serialize_field(discriminant, value)?; + } } state.end() } @@ -247,6 +287,8 @@ impl FactorSource { Self::sample_trusted_contact_radix(), Self::sample_security_questions(), Self::sample_security_questions_other(), + Self::sample_passphrase(), + Self::sample_passphrase_other(), ] } pub fn sample_device() -> Self { @@ -312,6 +354,14 @@ impl FactorSource { SecurityQuestions_NOT_PRODUCTION_READY_FactorSource::sample_other(), ) } + + pub fn sample_passphrase() -> Self { + Self::from(PassphraseFactorSource::sample()) + } + + pub fn sample_passphrase_other() -> Self { + Self::from(PassphraseFactorSource::sample_other()) + } } #[cfg(test)] @@ -422,6 +472,14 @@ mod tests { ); } + #[test] + fn factor_source_kind_passphrase() { + assert_eq!( + SUT::sample_passphrase().factor_source_kind(), + FactorSourceKind::Passphrase + ); + } + #[test] fn into_from_device() { let factor_source: SUT = DeviceFactorSource::sample().into(); diff --git a/crates/sargon/src/profile/v100/factors/factor_source_id_from_hash.rs b/crates/sargon/src/profile/v100/factors/factor_source_id_from_hash.rs index ff5dc83d6..998164633 100644 --- a/crates/sargon/src/profile/v100/factors/factor_source_id_from_hash.rs +++ b/crates/sargon/src/profile/v100/factors/factor_source_id_from_hash.rs @@ -1,5 +1,4 @@ use crate::prelude::*; -use radix_common::crypto::{blake2b_256_hash, Hash}; /// FactorSourceID from the blake2b hash of the special HD public key derived at `CAP26::GetID`, /// for a certain `FactorSourceKind` @@ -89,6 +88,15 @@ impl FactorSourceIDFromHash { mnemonic_with_passphrase, ) } + + pub fn new_for_passphrase( + mnemonic_with_passphrase: &MnemonicWithPassphrase, + ) -> Self { + Self::from_mnemonic_with_passphrase( + FactorSourceKind::Passphrase, + mnemonic_with_passphrase, + ) + } } impl FactorSourceIDFromHash { @@ -110,14 +118,60 @@ impl HasSampleValues for FactorSourceIDFromHash { } impl FactorSourceIDFromHash { - /// A sample used to facilitate unit tests. pub fn sample_device() -> Self { - DeviceFactorSource::sample().id + Self::new_for_device(&MnemonicWithPassphrase::sample_device()) + } + + pub fn sample_device_other() -> Self { + Self::new_for_device(&MnemonicWithPassphrase::sample_device_other()) } - /// A sample used to facilitate unit tests. pub fn sample_ledger() -> Self { - LedgerHardwareWalletFactorSource::sample().id + Self::new_for_ledger(&MnemonicWithPassphrase::sample_ledger()) + } + + pub fn sample_ledger_other() -> Self { + Self::new_for_ledger(&MnemonicWithPassphrase::sample_ledger_other()) + } + + pub fn sample_arculus() -> Self { + Self::new_for_arculus(&MnemonicWithPassphrase::sample_arculus()) + } + + pub fn sample_arculus_other() -> Self { + Self::new_for_arculus(&MnemonicWithPassphrase::sample_arculus_other()) + } + + pub fn sample_off_device() -> Self { + Self::new_for_off_device(&MnemonicWithPassphrase::sample_off_device()) + } + + pub fn sample_off_device_other() -> Self { + Self::new_for_off_device( + &MnemonicWithPassphrase::sample_off_device_other(), + ) + } + + pub fn sample_security_questions() -> Self { + Self::new_for_security_questions( + &MnemonicWithPassphrase::sample_security_questions(), + ) + } + + pub fn sample_security_questions_other() -> Self { + Self::new_for_security_questions( + &MnemonicWithPassphrase::sample_security_questions_other(), + ) + } + + pub fn sample_passphrase() -> Self { + Self::new_for_passphrase(&MnemonicWithPassphrase::sample_passphrase()) + } + + pub fn sample_passphrase_other() -> Self { + Self::new_for_passphrase( + &MnemonicWithPassphrase::sample_passphrase_other(), + ) } } diff --git a/crates/sargon/src/profile/v100/factors/factor_source_kind.rs b/crates/sargon/src/profile/v100/factors/factor_source_kind.rs index 79fa3920e..038162497 100644 --- a/crates/sargon/src/profile/v100/factors/factor_source_kind.rs +++ b/crates/sargon/src/profile/v100/factors/factor_source_kind.rs @@ -15,18 +15,6 @@ use crate::prelude::*; uniffi::Enum, )] pub enum FactorSourceKind { - /// A user owned unencrypted mnemonic (and optional BIP39 passphrase) stored on device, - /// thus directly usable. This kind is used as the standard factor source for all new - /// wallet users. - /// - /// Attributes: - /// * Mine - /// * On device - /// * Hierarchical deterministic (Mnemonic) - /// * Entity creating - #[serde(rename = "device")] - Device, - /// A user owned hardware wallet by vendor Ledger HQ, most commonly /// a Ledger Nano S or Ledger Nano X. Less common models are Ledger Nano S Plus /// Ledger Stax. @@ -40,24 +28,28 @@ pub enum FactorSourceKind { #[serde(rename = "ledgerHQHardwareWallet")] LedgerHQHardwareWallet, - /// A user owned mnemonic (and optional BIP39 passphrase) user has to input when used, - /// e.g. during signing. + /// An Arculus card, in credit card size, communicating with host using NFC. + /// + /// For more info see [link] /// /// Attributes: /// * Mine /// * Off device - /// * Hierarchical deterministic (Mnemonic) - #[serde(rename = "offDeviceMnemonic")] - OffDeviceMnemonic, + /// * Hierarchical deterministic (**Encrypted** mnemonic)\ + /// * Hardware (communicates with host using NFC) + /// + /// [link]: https://www.getarculus.com/ + #[serde(rename = "arculusCard")] + ArculusCard, - /// A contact, friend, company, organization or otherwise third party the user trusts enough - /// to be given a recovery token user has minted and sent the this contact. + /// Input key material for mnemonic (and optional BIP39 passphrase). /// /// Attributes: - /// * **Not** mine + /// * Mine /// * Off device - #[serde(rename = "trustedContact")] - TrustedContact, + /// * Hierarchical deterministic (IKM -> HKDF -> Mnemonic) + #[serde(rename = "passphrase")] + Passphrase, /// An encrypted user owned mnemonic (*never* any BIP39 passphrase) which can /// be decrypted by answers to **security question**, which are personal questions @@ -70,19 +62,36 @@ pub enum FactorSourceKind { #[serde(rename = "securityQuestions")] SecurityQuestions, - /// An Arculus card, in credit card size, communicating with host using NFC. - /// - /// For more info see [link] + /// A user owned mnemonic (and optional BIP39 passphrase) user has to input when used, + /// e.g. during signing. /// /// Attributes: /// * Mine /// * Off device - /// * Hierarchical deterministic (**Encrypted** mnemonic)\ - /// * Hardware (communicates with host using NFC) + /// * Hierarchical deterministic (Mnemonic) + #[serde(rename = "offDeviceMnemonic")] + OffDeviceMnemonic, + + /// A user owned unencrypted mnemonic (and optional BIP39 passphrase) stored on device, + /// thus directly usable. This kind is used as the standard factor source for all new + /// wallet users. /// - /// [link]: https://www.getarculus.com/ - #[serde(rename = "arculusCard")] - ArculusCard, + /// Attributes: + /// * Mine + /// * On device + /// * Hierarchical deterministic (Mnemonic) + /// * Entity creating + #[serde(rename = "device")] + Device, + + /// A contact, friend, company, organization or otherwise third party the user trusts enough + /// to be given a recovery token user has minted and sent the this contact. + /// + /// Attributes: + /// * **Not** mine + /// * Off device + #[serde(rename = "trustedContact")] + TrustedContact, } impl FactorSourceKind { @@ -145,6 +154,7 @@ mod tests { eq(OffDeviceMnemonic, "offDeviceMnemonic"); eq(TrustedContact, "trustedContact"); eq(SecurityQuestions, "securityQuestions"); + eq(Passphrase, "passphrase"); } #[test] @@ -193,6 +203,7 @@ mod tests { assert_eq!(SUT::OffDeviceMnemonic.discriminant(), "offDeviceMnemonic"); assert_eq!(SUT::TrustedContact.discriminant(), "trustedContact"); + assert_eq!(SUT::Passphrase.discriminant(), "passphrase"); } #[test] @@ -214,6 +225,7 @@ mod tests { format!("{}", SUT::TrustedContact.discriminant()), "trustedContact" ); + assert_eq!(format!("{}", SUT::Passphrase.discriminant()), "passphrase"); } #[test] @@ -235,6 +247,10 @@ mod tests { &SUT::OffDeviceMnemonic, json!("offDeviceMnemonic"), ); + assert_json_value_eq_after_roundtrip( + &SUT::Passphrase, + json!("passphrase"), + ); assert_json_roundtrip(&SUT::Device); } } diff --git a/crates/sargon/src/profile/v100/factors/hd_transaction_signing_factor_instance.rs b/crates/sargon/src/profile/v100/factors/hd_transaction_signing_factor_instance.rs index 2c545b4e7..48d6a2697 100644 --- a/crates/sargon/src/profile/v100/factors/hd_transaction_signing_factor_instance.rs +++ b/crates/sargon/src/profile/v100/factors/hd_transaction_signing_factor_instance.rs @@ -16,8 +16,7 @@ impl HDFactorInstanceTransactionSigning { value .derivation_path() .as_cap26() - .ok_or(CommonError::WrongEntityKindOfInFactorInstancesPath) - .map(|p| p.clone()) + .ok_or(CommonError::WrongEntityKindOfInFactorInstancesPath).cloned() .and_then(|p| { p.try_into() .map_err(|_| CommonError::WrongEntityKindOfInFactorInstancesPath) diff --git a/crates/sargon/src/profile/v100/factors/hierarchical_deterministic_factor_instance.rs b/crates/sargon/src/profile/v100/factors/hierarchical_deterministic_factor_instance.rs index 07f05f139..2dff159d6 100644 --- a/crates/sargon/src/profile/v100/factors/hierarchical_deterministic_factor_instance.rs +++ b/crates/sargon/src/profile/v100/factors/hierarchical_deterministic_factor_instance.rs @@ -35,6 +35,32 @@ impl HierarchicalDeterministicFactorInstance { ) } + pub fn new_for_entity( + factor_source_id: FactorSourceIDFromHash, + entity_kind: CAP26EntityKind, + index: HDPathComponent, + ) -> Self { + let derivation_path: DerivationPath = match entity_kind { + CAP26EntityKind::Account => AccountPath::new( + NetworkID::Mainnet, + CAP26KeyKind::TransactionSigning, + index.index(), + ) + .into(), + CAP26EntityKind::Identity => IdentityPath::new( + NetworkID::Mainnet, + CAP26KeyKind::TransactionSigning, + index.index(), + ) + .into(), + }; + + let seed = factor_source_id.sample_associated_mnemonic().to_seed(); + let hd_private_key = seed.derive_private_key(&derivation_path); + + Self::new(factor_source_id, hd_private_key.public_key()) + } + pub fn try_from( factor_source_id: FactorSourceID, public_key: PublicKey, diff --git a/crates/sargon/src/signing/collector/mod.rs b/crates/sargon/src/signing/collector/mod.rs new file mode 100644 index 000000000..081e29254 --- /dev/null +++ b/crates/sargon/src/signing/collector/mod.rs @@ -0,0 +1,13 @@ +mod signatures_collecting_continuation; +mod signatures_collector; +mod signatures_collector_dependencies; +mod signatures_collector_preprocessor; +mod signatures_collector_state; +mod signing_finish_early_strategy; + +pub(crate) use signatures_collector_preprocessor::*; + +pub use signatures_collecting_continuation::*; +pub use signatures_collector::*; +pub use signatures_collector_dependencies::*; +pub use signing_finish_early_strategy::*; diff --git a/crates/sargon/src/signing/collector/signatures_collecting_continuation.rs b/crates/sargon/src/signing/collector/signatures_collecting_continuation.rs new file mode 100644 index 000000000..45d6351bd --- /dev/null +++ b/crates/sargon/src/signing/collector/signatures_collecting_continuation.rs @@ -0,0 +1,14 @@ +/// Whether to continue collecting signatures or finish early. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SignaturesCollectingContinuation { + /// It is meaningless to continue collecting signatures, either since either + /// all transactions are valid, and the collector is configured to finish early + /// in that case, or some transaction is invalid and the collector is configured + /// finish early in that case. + FinishEarly, + + /// We should continue collecting signatures, either since the collector is + /// configured to not finish early, even though we can, or since we cannot + /// finish early since not enough factor sources have been signed with. + Continue, +} diff --git a/crates/sargon/src/signing/collector/signatures_collector.rs b/crates/sargon/src/signing/collector/signatures_collector.rs new file mode 100644 index 000000000..2451bfb8d --- /dev/null +++ b/crates/sargon/src/signing/collector/signatures_collector.rs @@ -0,0 +1,2314 @@ +use crate::prelude::*; + +use super::{ + signatures_collector_dependencies::*, signatures_collector_preprocessor::*, + signatures_collector_state::*, +}; + +use SignaturesCollectingContinuation::*; + +/// A coordinator which gathers signatures from several factor sources of different +/// kinds, in increasing friction order, for many transactions and for +/// potentially multiple entities and for many factor instances (derivation paths) +/// for each transaction. +/// +/// By increasing friction order we mean, the quickest and easiest to use FactorSourceKind +/// is last; namely `DeviceFactorSource`, and the most tedious FactorSourceKind is +/// first; namely `LedgerFactorSource`, which user might also lack access to. +pub struct SignaturesCollector { + /// Stateless immutable values used by the collector to gather signatures + /// from factor sources. + dependencies: SignaturesCollectorDependencies, + + /// Mutable internal state of the collector which builds up the list + /// of signatures from each used factor source. + state: RefCell, +} + +// === PUBLIC === +impl SignaturesCollector { + pub fn new( + finish_early_strategy: SigningFinishEarlyStrategy, + transactions: impl IntoIterator, + interactors: Arc, + profile: &Profile, + role_kind: RoleKind, + ) -> Result { + Self::with_signers_extraction( + finish_early_strategy, + IndexSet::from_iter(profile.factor_sources.iter()), + transactions, + interactors, + role_kind, + |i| TXToSign::extracting_from_intent_and_profile(&i, profile), + ) + } + + pub async fn collect_signatures(self) -> SignaturesOutcome { + let _ = self + .sign_with_factors() // in decreasing "friction order" + .await + .inspect_err(|e| error!("Failed to use factor sources: {:#?}", e)); + + self.outcome() + } +} + +// === INTERNAL === +impl SignaturesCollector { + pub(crate) fn with( + finish_early_strategy: SigningFinishEarlyStrategy, + profile_factor_sources: IndexSet, + transactions: IndexSet, + interactors: Arc, + role_kind: RoleKind, + ) -> Self { + debug!("Init SignaturesCollector"); + let preprocessor = SignaturesCollectorPreprocessor::new(transactions); + let (petitions, factors) = + preprocessor.preprocess(profile_factor_sources, role_kind); + + let dependencies = SignaturesCollectorDependencies::new( + finish_early_strategy, + interactors, + factors, + ); + let state = SignaturesCollectorState::new(petitions); + + Self { + dependencies, + state: RefCell::new(state), + } + } + + pub(crate) fn with_signers_extraction( + finish_early_strategy: SigningFinishEarlyStrategy, + all_factor_sources_in_profile: IndexSet, + transactions: impl IntoIterator, + interactors: Arc, + role_kind: RoleKind, + extract_signers: F, + ) -> Result + where + F: Fn(TransactionIntent) -> Result, + { + let transactions = transactions + .into_iter() + .map(extract_signers) + .collect::>>()?; + + let collector = Self::with( + finish_early_strategy, + all_factor_sources_in_profile, + transactions, + interactors, + role_kind, + ); + + Ok(collector) + } +} + +// === PRIVATE === +impl SignaturesCollector { + /// Returning `Continue` means that we should continue collecting signatures. + /// + /// Returning `FinishEarly` if it is meaningless to continue collecting signatures, + /// either since all transactions are valid and this collector is configured + /// to finish early in that case, or if some transaction is invalid and this + /// collector is configured to finish early in that case. + /// + /// N.B. this method does not concern itself with how many or which + /// factor sources are left to sign with, that is handled by the main loop, + /// i.e. this might return `Continue` even though there is not factor sources + /// left to sign with. + fn continuation(&self) -> SignaturesCollectingContinuation { + let finish_early_strategy = self.dependencies.finish_early_strategy; + let when_all_transactions_are_valid = + finish_early_strategy.when_all_transactions_are_valid.0; + let when_some_transaction_is_invalid = + finish_early_strategy.when_some_transaction_is_invalid.0; + + let petitions_status = self.state.borrow().petitions.borrow().status(); + + if petitions_status.are_all_valid() { + if when_all_transactions_are_valid == FinishEarly { + info!("All valid && should finish early => finish early"); + return FinishEarly; + } else { + debug!( + "All valid, BUT the collector is configured to NOT finish early => Continue" + ); + } + } else if petitions_status.is_some_invalid() { + if when_some_transaction_is_invalid == FinishEarly { + info!("Some invalid && should finish early => finish early"); + return FinishEarly; + } else { + debug!("Some transactions invalid, BUT the collector is configured to NOT finish early in case of failures => Continue"); + } + } + + Continue + } + + fn should_neglect_factors_due_to_irrelevant( + &self, + factor_sources_of_kind: &FactorSourcesOfKind, + ) -> bool { + let state = self.state.borrow(); + let petitions = state.petitions.borrow(); + petitions + .should_neglect_factors_due_to_irrelevant(factor_sources_of_kind) + } + + fn neglected_factors_due_to_irrelevant( + &self, + factor_sources_of_kind: &FactorSourcesOfKind, + ) -> bool { + if self.should_neglect_factors_due_to_irrelevant(factor_sources_of_kind) + { + info!( + "Neglecting all factors of kind: {} since they are all irrelevant (all TX referencing those factors have already failed)", + factor_sources_of_kind.kind + ); + self.process_batch_response(SignWithFactorsOutcome::irrelevant( + factor_sources_of_kind, + )); + true + } else { + false + } + } + + async fn sign_with_factors_of_kind( + &self, + factor_sources_of_kind: &FactorSourcesOfKind, + ) { + info!( + "Use(?) #{:?} factors of kind: {:?}", + &factor_sources_of_kind.factor_sources().len(), + &factor_sources_of_kind.kind + ); + + let interactor = self + .dependencies + .interactors + .interactor_for(factor_sources_of_kind.kind); + let factor_sources = factor_sources_of_kind.factor_sources(); + match interactor { + // PolyFactor Interactor: Many Factor Sources at once + SignInteractor::PolyFactor(interactor) => { + // Prepare the request for the interactor + debug!("Creating poly request for interactor"); + let request = self + .request_for_parallel_interactor(factor_sources_of_kind); + if !request.invalid_transactions_if_neglected.is_empty() { + info!( + "If factors {:?} are neglected, invalid TXs: {:?}", + request.per_factor_source.keys(), + request.invalid_transactions_if_neglected + ) + } + debug!("Dispatching poly request to interactor: {:?}", request); + let response = interactor.sign(request).await; + debug!("Got response from poly interactor: {:?}", response); + self.process_batch_response(response); + } + + // MonoFactor Interactor: One Factor Sources at a time + // After each factor source we pass the result to the collector + // updating its internal state so that we state about being able + // to skip the next factor source or not. + SignInteractor::MonoFactor(interactor) => { + for factor_source in factor_sources { + // Prepare the request for the interactor + debug!("Creating mono request for interactor"); + let request = self.request_for_serial_interactor( + factor_source + .factor_source_id() + .as_hash() + .expect("Signature Collector only works with HD FactorSources.") + ); + + if !request.invalid_transactions_if_neglected.is_empty() { + info!( + "If factor {:?} are neglected, invalid TXs: {:?}", + request.input.factor_source_id, + request.invalid_transactions_if_neglected + ) + } + + debug!( + "Dispatching mono request to interactor: {:?}", + request + ); + // Produce the results from the interactor + let response = interactor.sign(request).await; + debug!("Got response from mono interactor: {:?}", response); + + // Report the results back to the collector + self.process_batch_response(response); + + if self.continuation() == FinishEarly { + break; + } + } + } + } + } + + /// In decreasing "friction order" + async fn sign_with_factors(&self) -> Result<()> { + let factors_of_kind = self.dependencies.factors_of_kind.clone(); + for factor_sources_of_kind in factors_of_kind.iter() { + if self.continuation() == FinishEarly { + break; + } + if self.neglected_factors_due_to_irrelevant(factor_sources_of_kind) + { + continue; + } + self.sign_with_factors_of_kind(factor_sources_of_kind).await; + } + info!("FINISHED WITH ALL FACTORS"); + Ok(()) + } + + fn input_for_interactor( + &self, + factor_source_id: &FactorSourceIDFromHash, + ) -> MonoFactorSignRequestInput { + self.state + .borrow() + .petitions + .borrow() + .input_for_interactor(factor_source_id) + } + + fn request_for_serial_interactor( + &self, + factor_source_id: &FactorSourceIDFromHash, + ) -> MonoFactorSignRequest { + let batch_signing_request = self.input_for_interactor(factor_source_id); + + MonoFactorSignRequest::new( + batch_signing_request, + self.invalid_transactions_if_neglected_factor_sources( + IndexSet::just(*factor_source_id), + ) + .into_iter() + .collect::>(), + ) + } + + fn request_for_parallel_interactor( + &self, + factor_sources_of_kind: &FactorSourcesOfKind, + ) -> PolyFactorSignRequest { + let factor_source_ids = factor_sources_of_kind + .factor_sources() + .iter() + .map(|f| { + *f.factor_source_id().as_hash().expect( + "Signature Collector only works with HD FactorSources.", + ) + }) + .collect::>(); + let per_factor_source = factor_source_ids + .clone() + .iter() + .map(|fid| (*fid, self.input_for_interactor(fid))) + .collect::>(); + + let invalid_transactions_if_neglected = self + .invalid_transactions_if_neglected_factor_sources( + factor_source_ids, + ); + + // Prepare the request for the interactor + PolyFactorSignRequest::new( + factor_sources_of_kind.kind, + per_factor_source, + invalid_transactions_if_neglected, + ) + } + + fn invalid_transactions_if_neglected_factor_sources( + &self, + factor_source_ids: IndexSet, + ) -> IndexSet { + self.state + .borrow() + .petitions + .borrow() + .invalid_transactions_if_neglected_factors(factor_source_ids) + } + + fn process_batch_response(&self, response: SignWithFactorsOutcome) { + let state = self.state.borrow_mut(); + let petitions = state.petitions.borrow_mut(); + petitions.process_batch_response(response) + } + + fn outcome(self) -> SignaturesOutcome { + let expected_number_of_transactions; + { + let state = self.state.borrow_mut(); + let petitions = state.petitions.borrow_mut(); + expected_number_of_transactions = + petitions.txid_to_petition.borrow().len(); + } + let outcome = self.state.into_inner().petitions.into_inner().outcome(); + assert_eq!( + outcome.failed_transactions().len() + + outcome.successful_transactions().len(), + expected_number_of_transactions + ); + if !outcome.successful() { + warn!( + "Failed to sign, invalid tx: {:?}, petition", + outcome.failed_transactions() + ) + } + outcome + } +} +#[cfg(test)] +mod tests { + use std::iter; + + use super::*; + + impl SignaturesCollector { + /// Used by tests + pub(crate) fn petitions(self) -> Petitions { + self.state.into_inner().petitions.into_inner() + } + } + + #[test] + fn invalid_profile_unknown_account() { + let res = SignaturesCollector::new( + SigningFinishEarlyStrategy::default(), + [TransactionIntent::entities_requiring_auth( + [&Account::sample_at(0)], + [], + )], + Arc::new(TestSignatureCollectingInteractors::new( + SimulatedUser::prudent_no_fail(), + )), + &Profile::sample_from(IndexSet::new(), [], []), + RoleKind::Primary, + ); + assert!(matches!(res, Err(CommonError::UnknownAccount))); + } + + #[test] + fn invalid_profile_unknown_persona() { + let res = SignaturesCollector::new( + SigningFinishEarlyStrategy::default(), + [TransactionIntent::entities_requiring_auth( + [], + [&Persona::sample_at(0)], + )], + Arc::new(TestSignatureCollectingInteractors::new( + SimulatedUser::prudent_no_fail(), + )), + &Profile::sample_from(IndexSet::new(), [], []), + RoleKind::Primary, + ); + assert!(matches!(res, Err(CommonError::UnknownPersona))); + } + + #[actix_rt::test] + async fn valid_profile() { + let factors_sources = FactorSource::sample_all(); + let persona = Persona::sample_at(0); + + let collector = SignaturesCollector::new( + SigningFinishEarlyStrategy::default(), + [TransactionIntent::entities_requiring_auth([], [&persona])], + Arc::new(TestSignatureCollectingInteractors::new( + SimulatedUser::prudent_no_fail(), + )), + &Profile::sample_from(factors_sources, [], [&persona]), + RoleKind::Primary, + ) + .unwrap(); + let outcome = collector.collect_signatures().await; + assert!(outcome.successful()) + } + + #[actix_rt::test] + async fn continues_even_with_failed_tx_when_configured_to() { + let factor_sources = &FactorSource::sample_all(); + let a0 = &Account::sample_at(0); + let a1 = &Account::sample_at(1); + + let t0 = TransactionIntent::entities_requiring_auth([a1], []); + let t1 = TransactionIntent::entities_requiring_auth([a0], []); + + let profile = + Profile::sample_from(factor_sources.clone(), [a0, a1], []); + + let collector = SignaturesCollector::new( + SigningFinishEarlyStrategy::new( + WhenAllTransactionsAreValid(FinishEarly), + WhenSomeTransactionIsInvalid(Continue), + ), + [t0.clone(), t1.clone()], + Arc::new(TestSignatureCollectingInteractors::new( + SimulatedUser::prudent_with_failures( + SimulatedFailures::with_simulated_failures([ + FactorSourceIDFromHash::sample_at(1), + ]), + ), + )), + &profile, + RoleKind::Primary, + ) + .unwrap(); + + let outcome = collector.collect_signatures().await; + assert!(!outcome.successful()); + assert_eq!(outcome.failed_transactions().len(), 1); + assert_eq!(outcome.successful_transactions().len(), 1); + } + + #[actix_rt::test] + async fn continues_even_when_all_valid_if_configured_to() { + let test = async move |when_all_valid: WhenAllTransactionsAreValid, + expected_sig_count: usize| { + let factor_sources = &FactorSource::sample_all(); + let a5 = &Account::sample_at(5); + + let t0 = TransactionIntent::entities_requiring_auth([a5], []); + + let profile = + Profile::sample_from(factor_sources.clone(), [a5], []); + + let collector = SignaturesCollector::new( + SigningFinishEarlyStrategy::new( + when_all_valid, + WhenSomeTransactionIsInvalid::default(), + ), + [t0.clone()], + Arc::new(TestSignatureCollectingInteractors::new( + SimulatedUser::prudent_no_fail(), + )), + &profile, + RoleKind::Primary, + ) + .unwrap(); + + let outcome = collector.collect_signatures().await; + assert!(outcome.successful()); + assert_eq!( + outcome.signatures_of_successful_transactions().len(), + expected_sig_count + ); + }; + + test(WhenAllTransactionsAreValid(FinishEarly), 1).await; + test(WhenAllTransactionsAreValid(Continue), 2).await; + } + + #[test] + fn factor_source_kinds_order() { + let kinds = FactorSource::sample_all() + .into_iter() + .map(|f| f.factor_source_kind()) + .collect::>(); + let mut kinds = kinds.into_iter().collect_vec(); + kinds.sort(); + let kinds = kinds.into_iter().collect::>(); + assert_eq!( + kinds, + IndexSet::::from_iter([ + FactorSourceKind::LedgerHQHardwareWallet, + FactorSourceKind::ArculusCard, + FactorSourceKind::Passphrase, + FactorSourceKind::SecurityQuestions, + FactorSourceKind::OffDeviceMnemonic, + FactorSourceKind::Device, + ]) + ) + } + + #[test] + fn test_profile() { + let factor_sources = &FactorSource::sample_all(); + let a0 = &Account::sample_at(0); + let a1 = &Account::sample_at(1); + let a2 = &Account::sample_at(2); + let a6 = &Account::sample_at(6); + + let p0 = &Persona::sample_at(0); + let p1 = &Persona::sample_at(1); + let p2 = &Persona::sample_at(2); + let p6 = &Persona::sample_at(6); + + let t0 = TransactionIntent::entities_requiring_auth([a0, a1], [p0, p1]); + let t1 = TransactionIntent::entities_requiring_auth([a0, a1, a2], []); + let t2 = TransactionIntent::entities_requiring_auth([], [p0, p1, p2]); + let t3 = TransactionIntent::entities_requiring_auth([a6], [p6]); + + let profile = Profile::sample_from( + factor_sources.clone(), + [a0, a1, a2, a6], + [p0, p1, p2, p6], + ); + + let collector = SignaturesCollector::new( + SigningFinishEarlyStrategy::default(), + [t0.clone(), t1.clone(), t2.clone(), t3.clone()], + Arc::new(TestSignatureCollectingInteractors::new( + SimulatedUser::prudent_no_fail(), + )), + &profile, + RoleKind::Primary, + ) + .unwrap(); + + let petitions = collector.petitions(); + + assert_eq!(petitions.txid_to_petition.borrow().len(), 4); + + { + let petitions_ref = petitions.txid_to_petition.borrow(); + let petition = petitions_ref.get(&t3.intent_hash()).unwrap(); + let for_entities = petition.for_entities.borrow().clone(); + let pet6 = for_entities.get(&a6.address.into()).unwrap(); + + let paths6 = pet6 + .all_factor_instances() + .iter() + .map(|f| f.factor_instance().derivation_path()) + .collect_vec(); + + pretty_assertions::assert_eq!( + paths6, + iter::repeat_n( + DerivationPath::from(AccountPath::new( + NetworkID::Mainnet, + CAP26KeyKind::TransactionSigning, + 6 + )), + 5 + ) + .collect_vec() + ); + } + + let assert_petition = |t: &TransactionIntent, + threshold_factors: HashMap< + AddressOfAccountOrPersona, + HashSet, + >, + override_factors: HashMap< + AddressOfAccountOrPersona, + HashSet, + >| { + let petitions_ref = petitions.txid_to_petition.borrow(); + let petition = petitions_ref.get(&t.intent_hash()).unwrap(); + assert_eq!(petition.intent_hash, t.intent_hash()); + + let mut addresses = + threshold_factors.keys().collect::>(); + addresses.extend(override_factors.keys().collect::>()); + + assert_eq!( + petition + .for_entities + .borrow() + .keys() + .collect::>(), + addresses + ); + + assert!(petition + .for_entities + .borrow() + .iter() + .all(|(a, p)| { p.entity == *a })); + + assert!(petition + .for_entities + .borrow() + .iter() + .all(|(_, p)| { p.intent_hash == t.intent_hash() })); + + for (k, v) in petition.for_entities.borrow().iter() { + let threshold = threshold_factors.get(k); + if let Some(actual_threshold) = &v.threshold_factors { + let threshold = threshold.unwrap().clone(); + assert_eq!( + actual_threshold + .borrow() + .factor_instances() + .into_iter() + .map(|f| f.factor_source_id) + .collect::>(), + threshold + ); + } else { + assert!(threshold.is_none()); + } + + let override_ = override_factors.get(k); + if let Some(actual_override) = &v.override_factors { + let override_ = override_.unwrap().clone(); + assert_eq!( + actual_override + .borrow() + .factor_instances() + .into_iter() + .map(|f| f.factor_source_id) + .collect::>(), + override_ + ); + } else { + assert!(override_.is_none()); + } + } + }; + assert_petition( + &t0, + HashMap::from_iter([ + ( + a0.address.into(), + HashSet::just(FactorSourceIDFromHash::sample_at(0)), + ), + ( + a1.address.into(), + HashSet::just(FactorSourceIDFromHash::sample_at(1)), + ), + ( + p0.address.into(), + HashSet::just(FactorSourceIDFromHash::sample_at(0)), + ), + ( + p1.address.into(), + HashSet::just(FactorSourceIDFromHash::sample_at(1)), + ), + ]), + HashMap::new(), + ); + + assert_petition( + &t1, + HashMap::from_iter([ + ( + a0.address.into(), + HashSet::just(FactorSourceIDFromHash::sample_at(0)), + ), + ( + a1.address.into(), + HashSet::just(FactorSourceIDFromHash::sample_at(1)), + ), + ( + a2.address.into(), + HashSet::just(FactorSourceIDFromHash::sample_at(0)), + ), + ]), + HashMap::new(), + ); + + assert_petition( + &t2, + HashMap::from_iter([ + ( + p0.address.into(), + HashSet::just(FactorSourceIDFromHash::sample_at(0)), + ), + ( + p1.address.into(), + HashSet::just(FactorSourceIDFromHash::sample_at(1)), + ), + ( + p2.address.into(), + HashSet::just(FactorSourceIDFromHash::sample_at(0)), + ), + ]), + HashMap::new(), + ); + + assert_petition( + &t3, + HashMap::from_iter([ + ( + a6.address.into(), + HashSet::from_iter([ + FactorSourceIDFromHash::sample_at(0), + FactorSourceIDFromHash::sample_at(3), + FactorSourceIDFromHash::sample_at(5), + ]), + ), + ( + p6.address.into(), + HashSet::from_iter([ + FactorSourceIDFromHash::sample_at(0), + FactorSourceIDFromHash::sample_at(3), + FactorSourceIDFromHash::sample_at(5), + ]), + ), + ]), + HashMap::from_iter([ + ( + a6.address.into(), + HashSet::from_iter([ + FactorSourceIDFromHash::sample_at(1), + FactorSourceIDFromHash::sample_at(4), + ]), + ), + ( + p6.address.into(), + HashSet::from_iter([ + FactorSourceIDFromHash::sample_at(1), + FactorSourceIDFromHash::sample_at(4), + ]), + ), + ]), + ); + } + + mod multi_tx { + use super::*; + + async fn multi_accounts_multi_personas_all_single_factor_controlled_with_sim_user( + sim: SimulatedUser, + ) { + let factor_sources = &FactorSource::sample_all(); + let a0 = Account::sample_at(0); + let a1 = Account::sample_at(1); + let a2 = Account::sample_at(2); + + let p0 = Persona::sample_at(0); + let p1 = Persona::sample_at(1); + let p2 = Persona::sample_at(2); + + let t0 = TransactionIntent::entities_requiring_auth( + [&a0, &a1], + [&p0, &p1], + ); + let t1 = + TransactionIntent::entities_requiring_auth([&a0, &a1, &a2], []); + let t2 = + TransactionIntent::entities_requiring_auth([], [&p0, &p1, &p2]); + + let profile = Profile::sample_from( + factor_sources.clone(), + [&a0, &a1, &a2], + [&p0, &p1, &p2], + ); + + let collector = SignaturesCollector::new( + SigningFinishEarlyStrategy::default(), + [t0.clone(), t1.clone(), t2.clone()], + Arc::new(TestSignatureCollectingInteractors::new(sim)), + &profile, + RoleKind::Primary, + ) + .unwrap(); + + let outcome = collector.collect_signatures().await; + assert!(outcome.successful()); + assert!(outcome.failed_transactions().is_empty()); + assert_eq!( + outcome.signatures_of_successful_transactions().len(), + 10 + ); + assert_eq!( + outcome + .successful_transactions() + .into_iter() + .map(|t| t.intent_hash) + .collect::>(), + HashSet::from_iter([ + t0.clone().intent_hash(), + t1.clone().intent_hash(), + t2.clone().intent_hash(), + ]) + ); + let st0 = outcome + .successful_transactions() + .into_iter() + .find(|st| st.intent_hash == t0.intent_hash()) + .unwrap(); + + assert_eq!( + st0.signatures + .clone() + .into_iter() + .map(|s| s.owned_factor_instance().owner) + .collect::>(), + HashSet::from_iter([ + AddressOfAccountOrPersona::from(a0.address), + AddressOfAccountOrPersona::from(a1.address), + AddressOfAccountOrPersona::from(p0.address), + AddressOfAccountOrPersona::from(p1.address), + ]) + ); + + let st1 = outcome + .successful_transactions() + .into_iter() + .find(|st| st.intent_hash == t1.intent_hash()) + .unwrap(); + + assert_eq!( + st1.signatures + .clone() + .into_iter() + .map(|s| s.owned_factor_instance().owner) + .collect::>(), + HashSet::from_iter([ + AddressOfAccountOrPersona::from(a0.address), + AddressOfAccountOrPersona::from(a1.address), + AddressOfAccountOrPersona::from(a2.address), + ]) + ); + + let st2 = outcome + .successful_transactions() + .into_iter() + .find(|st| st.intent_hash == t2.intent_hash()) + .unwrap(); + + assert_eq!( + st2.signatures + .clone() + .into_iter() + .map(|s| s.owned_factor_instance().owner) + .collect::>(), + HashSet::from_iter([ + AddressOfAccountOrPersona::from(p0.address), + AddressOfAccountOrPersona::from(p1.address), + AddressOfAccountOrPersona::from(p2.address), + ]) + ); + + // Assert sorted in increasing "friction order". + assert_eq!( + outcome + .signatures_of_successful_transactions() + .iter() + .map(|f| { f.factor_source_id().kind }) + .collect::>(), + IndexSet::::from_iter([ + FactorSourceKind::Device, + FactorSourceKind::LedgerHQHardwareWallet + ]) + ); + } + + #[derive(Clone, Debug)] + struct Vector { + simulated_user: SimulatedUser, + expected: Expected, + } + #[derive(Clone, Debug, PartialEq, Eq)] + struct Expected { + successful_txs_signature_count: usize, + signed_factor_source_kinds: IndexSet, + expected_neglected_factor_source_count: usize, + } + async fn multi_securified_entities_with_sim_user(vector: Vector) { + let factor_sources = &FactorSource::sample_all(); + + let a4 = &Account::sample_at(4); + let a5 = &Account::sample_at(5); + let a6 = &Account::sample_at(6); + + let p4 = &Persona::sample_at(4); + let p5 = &Persona::sample_at(5); + let p6 = &Persona::sample_at(6); + + let t0 = TransactionIntent::entities_requiring_auth([a5], [p5]); + let t1 = + TransactionIntent::entities_requiring_auth([a4, a5, a6], []); + let t2 = + TransactionIntent::entities_requiring_auth([a4, a6], [p4, p6]); + let t3 = + TransactionIntent::entities_requiring_auth([], [p4, p5, p6]); + + let profile = Profile::sample_from( + factor_sources.clone(), + [a4, a5, a6], + [p4, p5, p6], + ); + + let collector = SignaturesCollector::new( + SigningFinishEarlyStrategy::default(), + [t0.clone(), t1.clone(), t2.clone(), t3.clone()], + Arc::new(TestSignatureCollectingInteractors::new( + vector.simulated_user, + )), + &profile, + RoleKind::Primary, + ) + .unwrap(); + + let outcome = collector.collect_signatures().await; + + assert_eq!( + outcome.neglected_factor_sources().len(), + vector.expected.expected_neglected_factor_source_count + ); + + assert!(outcome.successful()); + assert!(outcome.failed_transactions().is_empty()); + assert_eq!( + outcome.signatures_of_successful_transactions().len(), + vector.expected.successful_txs_signature_count + ); + assert_eq!( + outcome + .successful_transactions() + .into_iter() + .map(|t| t.intent_hash) + .collect::>(), + HashSet::from_iter([ + t0.clone().intent_hash(), + t1.clone().intent_hash(), + t2.clone().intent_hash(), + t3.clone().intent_hash(), + ]) + ); + + // Assert sorted in increasing "friction order". + assert_eq!( + outcome + .signatures_of_successful_transactions() + .iter() + .map(|f| { f.factor_source_id().kind }) + .collect::>(), + vector.expected.signed_factor_source_kinds + ); + } + + mod with_failure { + use std::rc::Rc; + + use super::*; + + #[actix_rt::test] + async fn multi_securified_entities() { + multi_securified_entities_with_sim_user(Vector { + simulated_user: SimulatedUser::prudent_with_failures( + SimulatedFailures::with_simulated_failures([ + FactorSourceIDFromHash::sample_at(1), + ]), + ), + expected: Expected { + successful_txs_signature_count: 24, + // We always end early + // `Device` FactorSourceKind never got used since it + // we are done after Passphrase. + signed_factor_source_kinds: + IndexSet::::from_iter([ + FactorSourceKind::ArculusCard, + FactorSourceKind::Passphrase, + ]), + expected_neglected_factor_source_count: 1, + }, + }) + .await; + } + + #[actix_rt::test] + async fn failed_threshold_successful_override() { + let factor_sources = &FactorSource::sample_all(); + let a9 = &Account::sample_at(9); + let tx0 = TransactionIntent::entities_requiring_auth([a9], []); + + let all_transactions = [tx0.clone()]; + + let profile = + Profile::sample_from(factor_sources.clone(), [a9], []); + + let collector = SignaturesCollector::new( + SigningFinishEarlyStrategy::default(), + all_transactions, + Arc::new(TestSignatureCollectingInteractors::new( + SimulatedUser::prudent_with_failures( + SimulatedFailures::with_simulated_failures([ + FactorSourceIDFromHash::sample_at(1), + ]), + ), + )), + &profile, + RoleKind::Primary, + ) + .unwrap(); + + let outcome = collector.collect_signatures().await; + assert!(outcome.successful()); + assert_eq!( + outcome + .successful_transactions() + .into_iter() + .map(|t| t.intent_hash.clone()) + .collect_vec(), + vec![tx0.clone().intent_hash()] + ); + assert_eq!( + outcome + .all_signatures() + .into_iter() + .map(|s| s.factor_source_id()) + .collect_vec(), + vec![FactorSourceIDFromHash::sample_at(8)] + ); + } + + #[actix_rt::test] + async fn many_failing_tx() { + let factor_sources = &FactorSource::sample_all(); + let a0 = &Account::sample_at(0); + let p3 = &Persona::sample_at(3); + let tx = TransactionIntent::entities_requiring_auth([], [p3]); + let failing_transactions = (0..100) + .map(|_| { + TransactionIntent::entities_requiring_auth([a0], []) + }) + .collect::>(); + let mut all_transactions = failing_transactions.clone(); + all_transactions.push(tx.clone()); + + let profile = + Profile::sample_from(factor_sources.clone(), [a0], [p3]); + + let collector = SignaturesCollector::new( + SigningFinishEarlyStrategy::default(), + all_transactions, + Arc::new(TestSignatureCollectingInteractors::new( + SimulatedUser::prudent_with_failures( + SimulatedFailures::with_simulated_failures([ + FactorSourceIDFromHash::sample_at(0), + ]), + ), + )), + &profile, + RoleKind::Primary, + ) + .unwrap(); + + let outcome = collector.collect_signatures().await; + assert!(!outcome.successful()); + assert_eq!( + outcome + .failed_transactions() + .iter() + .map(|t| t.intent_hash.clone()) + .collect_vec(), + failing_transactions + .iter() + .map(|t| t.intent_hash().clone()) + .collect_vec() + ); + + assert_eq!( + outcome + .ids_of_neglected_factor_sources_failed() + .into_iter() + .collect_vec(), + vec![FactorSourceIDFromHash::sample_at(0)] + ); + + assert!(outcome + .ids_of_neglected_factor_sources_skipped_by_user() + .is_empty()); + + assert_eq!( + outcome + .successful_transactions() + .into_iter() + .map(|t| t.intent_hash) + .collect_vec(), + vec![tx.intent_hash()] + ) + } + + #[actix_rt::test] + async fn same_tx_is_not_shown_to_user_in_case_of_already_failure() { + let factor_sources = FactorSource::sample_all(); + + let a7 = Account::sample_at(7); + let a0 = Account::sample_at(0); + + let tx0 = + TransactionIntent::entities_requiring_auth([&a7, &a0], []); + let tx1 = TransactionIntent::entities_requiring_auth([&a0], []); + + let profile = Profile::sample_from( + factor_sources.clone(), + [&a7, &a0], + [], + ); + + type Tuple = + (FactorSourceKind, IndexSet); + type Tuples = Vec; + let tuples = + Rc::>::new(RefCell::new(Tuples::default())); + let tuples_clone = tuples.clone(); + let collector = SignaturesCollector::new( + SigningFinishEarlyStrategy::default(), + [tx0.clone(), tx1.clone()], + Arc::new(TestSignatureCollectingInteractors::new( + SimulatedUser::with_spy( + move |kind, invalid| { + let tuple = (kind, invalid); + let mut x = RefCell::borrow_mut(&tuples_clone); + x.push(tuple) + }, + SimulatedUserMode::Prudent, + SimulatedFailures::with_simulated_failures([ + FactorSourceIDFromHash::sample_at(2), // will cause any TX with a7 to fail + ]), + ), + )), + &profile, + RoleKind::Primary, + ) + .unwrap(); + + let outcome = collector.collect_signatures().await; + + let tuples = tuples.borrow().clone(); + assert_eq!( + tuples, + vec![ + ( + FactorSourceKind::LedgerHQHardwareWallet, + IndexSet::just(InvalidTransactionIfNeglected::new( + tx0.clone().intent_hash(), + [a7.address.into()] + )) + ), + // Important that we do NOT display any mentioning of `tx0` here again! + ( + FactorSourceKind::Device, + IndexSet::just(InvalidTransactionIfNeglected::new( + tx1.clone().intent_hash(), + [a0.address.into()] + )) + ), + ] + ); + + assert!(!outcome.successful()); + assert_eq!( + outcome.ids_of_neglected_factor_sources_failed(), + IndexSet::::just( + FactorSourceIDFromHash::sample_at(2) + ) + ); + assert_eq!( + outcome.ids_of_neglected_factor_sources_irrelevant(), + IndexSet::::from_iter([ + FactorSourceIDFromHash::sample_at(6), + FactorSourceIDFromHash::sample_at(7), + FactorSourceIDFromHash::sample_at(8), + FactorSourceIDFromHash::sample_at(9) + ]) + ); + assert_eq!( + outcome + .successful_transactions() + .into_iter() + .map(|t| t.intent_hash) + .collect_vec(), + vec![tx1.intent_hash().clone()] + ); + + assert_eq!( + outcome + .failed_transactions() + .into_iter() + .map(|t| t.intent_hash) + .collect_vec(), + vec![tx0.intent_hash().clone()] + ); + + assert_eq!(outcome.all_signatures().len(), 1); + + assert!(outcome + .all_signatures() + .into_iter() + .map(|s| s.intent_hash().clone()) + .all(|i| i == tx1.intent_hash())); + + assert_eq!( + outcome + .all_signatures() + .into_iter() + .map(|s| s.derivation_path()) + .collect_vec(), + vec![DerivationPath::from(AccountPath::new( + NetworkID::Mainnet, + CAP26KeyKind::TransactionSigning, + 0 + ))] + ) + } + } + + mod no_fail { + use super::*; + + #[actix_rt::test] + async fn multi_accounts_multi_personas_all_single_factor_controlled( + ) { + multi_accounts_multi_personas_all_single_factor_controlled_with_sim_user( + SimulatedUser::prudent_no_fail(), + ) + .await; + + // Same result with lazy user, not able to skip without failures. + multi_accounts_multi_personas_all_single_factor_controlled_with_sim_user( + SimulatedUser::lazy_sign_minimum([]), + ) + .await + } + + #[actix_rt::test] + async fn multi_securified_entities() { + multi_securified_entities_with_sim_user(Vector { + simulated_user: SimulatedUser::prudent_no_fail(), + expected: Expected { + successful_txs_signature_count: 32, + // We always end early + // `Device` FactorSourceKind never got used since it + // we are done after YubiKey. + signed_factor_source_kinds: + IndexSet::::from_iter([ + FactorSourceKind::LedgerHQHardwareWallet, + FactorSourceKind::ArculusCard, + FactorSourceKind::Passphrase, + ]), + expected_neglected_factor_source_count: 0, + }, + }) + .await; + + multi_securified_entities_with_sim_user(Vector { + simulated_user: SimulatedUser::lazy_sign_minimum([]), + expected: Expected { + successful_txs_signature_count: 24, + // We always end early, this lazy user was able to skip + // Ledger. + signed_factor_source_kinds: + IndexSet::::from_iter([ + FactorSourceKind::ArculusCard, + FactorSourceKind::Passphrase, + FactorSourceKind::Device, + ]), + expected_neglected_factor_source_count: 2, + }, + }) + .await; + } + } + } + + mod single_tx { + use super::*; + + mod multiple_entities { + use super::*; + + #[actix_rt::test] + async fn prudent_user_single_tx_two_accounts_same_factor_source() { + let collector = SignaturesCollector::test_prudent([TXToSign::sample([ + Account::sample_unsecurified_mainnet( + "A0", + HierarchicalDeterministicFactorInstance::new_for_entity( + FactorSourceIDFromHash::sample_at(0), + CAP26EntityKind::Account, + HDPathComponent::from(0), + ), + ), + Account::sample_unsecurified_mainnet( + "A1", + HierarchicalDeterministicFactorInstance::new_for_entity( + FactorSourceIDFromHash::sample_at(0), + CAP26EntityKind::Account, + HDPathComponent::from(1), + ), + ), + ])]); + + let outcome = collector.collect_signatures().await; + assert!(outcome.successful()); + let signatures = outcome.all_signatures(); + assert_eq!(signatures.len(), 2); + assert_eq!( + signatures + .into_iter() + .map(|s| s.derivation_path()) + .collect::>(), + [ + DerivationPath::from(AccountPath::new( + NetworkID::Mainnet, + CAP26KeyKind::TransactionSigning, + 0 + )), + DerivationPath::from(AccountPath::new( + NetworkID::Mainnet, + CAP26KeyKind::TransactionSigning, + 1 + )), + ] + .into_iter() + .collect::>() + ) + } + + #[actix_rt::test] + async fn prudent_user_single_tx_two_accounts_different_factor_sources( + ) { + let collector = + SignaturesCollector::test_prudent([TXToSign::sample([ + Account::sample_at(0), + Account::sample_at(1), + ])]); + + let outcome = collector.collect_signatures().await; + assert!(outcome.successful()); + let signatures = outcome.all_signatures(); + assert_eq!(signatures.len(), 2); + } + } + + mod single_entity { + use super::*; + use std::any::TypeId; + + fn sample_at( + index: usize, + ) -> AccountOrPersona { + if TypeId::of::() == TypeId::of::() { + AccountOrPersona::AccountEntity(Account::sample_at(index)) + } else { + AccountOrPersona::PersonaEntity(Persona::sample_at(index)) + } + } + + fn sample_securified_mainnet( + name: impl AsRef, + make_role: impl Fn() -> GeneralRoleWithHierarchicalDeterministicFactorInstances, + ) -> AccountOrPersona { + if TypeId::of::() == TypeId::of::() { + AccountOrPersona::from(Account::sample_securified_mainnet( + name, + AccountAddress::sample(), + make_role, + )) + } else { + AccountOrPersona::from(Persona::sample_securified_mainnet( + name, + IdentityAddress::sample(), + make_role, + )) + } + } + + impl AccountOrPersona { + fn transaction_signing_factor_instances( + &self, + ) -> IndexSet { + let sec_state: EntitySecurityState = match self { + AccountOrPersona::AccountEntity(account) => { + account.security_state.clone() + } + AccountOrPersona::PersonaEntity(persona) => { + persona.security_state.clone() + } + }; + + match sec_state { + EntitySecurityState::Unsecured { value } => { + IndexSet::from_iter([value + .transaction_signing + .factor_instance()]) + } + EntitySecurityState::Securified { value } => { + let matrix = value + .security_structure + .matrix_of_factors + .clone(); + let mut set = IndexSet::new(); + set.extend(matrix.primary_role.threshold_factors); + set.extend(matrix.primary_role.override_factors); + set + } + } + } + } + + async fn prudent_user_single_tx_e0() { + let collector = SignaturesCollector::test_prudent([ + TXToSign::sample([sample_at::(0)]), + ]); + let outcome = collector.collect_signatures().await; + assert!(outcome.successful()); + let signatures = outcome.all_signatures(); + assert_eq!(signatures.len(), 1); + } + + async fn prudent_user_single_tx_e0_assert_correct_intent_hash_is_signed< + E: IsEntity + 'static, + >() { + let sample = sample_at::(0); + let tx = TXToSign::sample([sample.clone()]); + let collector = SignaturesCollector::test_prudent([tx.clone()]); + let signature = + &collector.collect_signatures().await.all_signatures()[0]; + assert_eq!(signature.intent_hash(), &tx.intent_hash); + + if sample.is_account_entity() { + assert_eq!( + signature + .derivation_path() + .as_cap26() + .unwrap() + .as_account() + .unwrap() + .entity_kind, + CAP26EntityKind::Account + ); + } else { + assert_eq!( + signature + .derivation_path() + .as_cap26() + .unwrap() + .as_identity() + .unwrap() + .entity_kind, + CAP26EntityKind::Identity + ); + } + } + + async fn prudent_user_single_tx_e0_assert_correct_owner_has_signed< + E: IsEntity + 'static, + >() { + let entity = sample_at::(0); + let tx = TXToSign::sample([entity.clone()]); + let collector = SignaturesCollector::test_prudent([tx.clone()]); + let signature = + &collector.collect_signatures().await.all_signatures()[0]; + assert_eq!( + signature.owned_factor_instance().owner, + entity.address() + ); + } + + async fn prudent_user_single_tx_e0_assert_correct_owner_factor_instance_signed< + E: IsEntity + 'static, + >() { + let entity = sample_at::(0); + let tx = TXToSign::sample([entity.clone()]); + let collector = SignaturesCollector::test_prudent([tx.clone()]); + let signature = + &collector.collect_signatures().await.all_signatures()[0]; + + assert_eq!( + signature + .owned_factor_instance() + .factor_instance() + .factor_instance(), + entity + .transaction_signing_factor_instances() + .first() + .unwrap() + .clone() + ); + } + + async fn prudent_user_single_tx_e1() { + let collector = SignaturesCollector::test_prudent([ + TXToSign::sample([sample_at::(1)]), + ]); + let outcome = collector.collect_signatures().await; + assert!(outcome.successful()); + let signatures = outcome.all_signatures(); + assert_eq!(signatures.len(), 1); + } + + async fn prudent_user_single_tx_e2() { + let collector = SignaturesCollector::test_prudent([ + TXToSign::sample([sample_at::(2)]), + ]); + let outcome = collector.collect_signatures().await; + assert!(outcome.successful()); + let signatures = outcome.all_signatures(); + assert_eq!(signatures.len(), 1); + } + + async fn prudent_user_single_tx_e3() { + let collector = SignaturesCollector::test_prudent([ + TXToSign::sample([sample_at::(3)]), + ]); + let outcome = collector.collect_signatures().await; + assert!(outcome.successful()); + let signatures = outcome.all_signatures(); + assert_eq!(signatures.len(), 1); + } + + async fn prudent_user_single_tx_e4() { + let collector = SignaturesCollector::test_prudent([ + TXToSign::sample([sample_at::(4)]), + ]); + let outcome = collector.collect_signatures().await; + assert!(outcome.successful()); + let signatures = outcome.all_signatures(); + assert_eq!(signatures.len(), 2); + } + + async fn prudent_user_single_tx_e5() { + let collector = SignaturesCollector::test_prudent([ + TXToSign::sample([sample_at::(5)]), + ]); + let outcome = collector.collect_signatures().await; + assert!(outcome.successful()); + let signatures = outcome.all_signatures(); + assert_eq!(signatures.len(), 1); + } + + async fn prudent_user_single_tx_e6() { + let collector = SignaturesCollector::test_prudent([ + TXToSign::sample([sample_at::(6)]), + ]); + let outcome = collector.collect_signatures().await; + assert!(outcome.successful()); + let signatures = outcome.all_signatures(); + assert_eq!(signatures.len(), 1); + } + + async fn prudent_user_single_tx_e7() { + let collector = SignaturesCollector::test_prudent([ + TXToSign::sample([sample_at::(7)]), + ]); + let outcome = collector.collect_signatures().await; + assert!(outcome.successful()); + let signatures = outcome.all_signatures(); + + assert_eq!(signatures.len(), 5); + } + + async fn lazy_sign_minimum_user_single_tx_e0< + E: IsEntity + 'static, + >() { + let collector = + SignaturesCollector::test_lazy_sign_minimum_no_failures([ + TXToSign::sample([sample_at::(0)]), + ]); + let outcome = collector.collect_signatures().await; + assert!(outcome.successful()); + let signatures = outcome.all_signatures(); + assert_eq!(signatures.len(), 1); + } + + async fn lazy_sign_minimum_user_single_tx_e1< + E: IsEntity + 'static, + >() { + let collector = + SignaturesCollector::test_lazy_sign_minimum_no_failures([ + TXToSign::sample([sample_at::(1)]), + ]); + let outcome = collector.collect_signatures().await; + assert!(outcome.successful()); + let signatures = outcome.all_signatures(); + assert_eq!(signatures.len(), 1); + } + + async fn lazy_sign_minimum_user_single_tx_e2< + E: IsEntity + 'static, + >() { + let collector = + SignaturesCollector::test_lazy_sign_minimum_no_failures([ + TXToSign::sample([sample_at::(2)]), + ]); + let outcome = collector.collect_signatures().await; + assert!(outcome.successful()); + let signatures = outcome.all_signatures(); + assert_eq!(signatures.len(), 1); + } + + async fn lazy_sign_minimum_user_e3() { + let collector = + SignaturesCollector::test_lazy_sign_minimum_no_failures([ + TXToSign::sample([sample_at::(3)]), + ]); + let outcome = collector.collect_signatures().await; + assert!(outcome.successful()); + let signatures = outcome.all_signatures(); + assert_eq!(signatures.len(), 1); + } + + async fn lazy_sign_minimum_user_e4() { + let collector = + SignaturesCollector::test_lazy_sign_minimum_no_failures([ + TXToSign::sample([sample_at::(4)]), + ]); + let outcome = collector.collect_signatures().await; + assert!(outcome.successful()); + let signatures = outcome.all_signatures(); + assert_eq!(signatures.len(), 2); + } + + async fn lazy_sign_minimum_user_e5() { + let collector = + SignaturesCollector::test_lazy_sign_minimum_no_failures([ + TXToSign::sample([sample_at::(5)]), + ]); + let outcome = collector.collect_signatures().await; + assert!(outcome.successful()); + let signatures = outcome.all_signatures(); + assert_eq!(signatures.len(), 1); + } + + async fn lazy_sign_minimum_user_e6() { + let collector = + SignaturesCollector::test_lazy_sign_minimum_no_failures([ + TXToSign::sample([sample_at::(6)]), + ]); + let outcome = collector.collect_signatures().await; + assert!(outcome.successful()); + let signatures = outcome.all_signatures(); + + assert_eq!(signatures.len(), 2); + } + + async fn lazy_sign_minimum_user_e7() { + let collector = + SignaturesCollector::test_lazy_sign_minimum_no_failures([ + TXToSign::sample([sample_at::(7)]), + ]); + let outcome = collector.collect_signatures().await; + assert!(outcome.successful()); + let signatures = outcome.all_signatures(); + + assert_eq!(signatures.len(), 5); + } + + async fn lazy_sign_minimum_user_e5_last_factor_used< + E: IsEntity + 'static, + >() { + let entity = sample_at::(5); + let collector = + SignaturesCollector::test_lazy_sign_minimum_no_failures([ + TXToSign::sample([entity.clone()]), + ]); + let outcome = collector.collect_signatures().await; + assert!(outcome.successful()); + let signatures = outcome.all_signatures(); + assert_eq!(signatures.len(), 1); + + let signature = &signatures[0]; + + assert_eq!( + signature + .owned_factor_instance() + .factor_instance() + .factor_source_id, + FactorSourceIDFromHash::sample_at(4) + ); + + assert_eq!( + outcome.ids_of_neglected_factor_sources(), + IndexSet::just(FactorSourceIDFromHash::sample_at(1)) + ) + } + + async fn lazy_sign_minimum_all_known_factors_used_as_override_factors_signed_with_device_for_entity< + E: IsEntity + 'static, + >() { + let collector = + SignaturesCollector::test_lazy_sign_minimum_no_failures([ + TXToSign::sample([sample_securified_mainnet::( + "Alice", + || { + GeneralRoleWithHierarchicalDeterministicFactorInstances::override_only( + FactorSource::sample_all().into_iter().map(|f| { + HierarchicalDeterministicFactorInstance::sample_mainnet_tx_account( + HDPathComponent::from(0), + *f.factor_source_id().as_hash().unwrap(), + ) + }), + ) + }, + )]), + ]); + let outcome = collector.collect_signatures().await; + assert!(outcome.successful()); + let signatures = outcome.all_signatures(); + assert_eq!(signatures.len(), 2); + + assert!(signatures + .into_iter() + .all(|s| s.factor_source_id().kind + == FactorSourceKind::Device)); + } + + async fn lazy_always_skip_user_single_tx_e0< + E: IsEntity + 'static, + >() { + let collector = SignaturesCollector::test_lazy_always_skip([ + TXToSign::sample([sample_at::(0)]), + ]); + let outcome = collector.collect_signatures().await; + assert!(!outcome.successful()); + let signatures = outcome.all_signatures(); + assert!(signatures.is_empty()); + } + + async fn fail_get_neglected_e0() { + let failing = + IndexSet::<_>::just(FactorSourceIDFromHash::sample_at(0)); + let collector = SignaturesCollector::test_prudent_with_failures( + [TXToSign::sample([sample_at::(0)])], + SimulatedFailures::with_simulated_failures(failing.clone()), + ); + let outcome = collector.collect_signatures().await; + assert!(!outcome.successful()); + let neglected = outcome.ids_of_neglected_factor_sources(); + assert_eq!(neglected, failing); + } + + async fn lazy_always_skip_user_single_tx_e1< + E: IsEntity + 'static, + >() { + let collector = SignaturesCollector::test_lazy_always_skip([ + TXToSign::sample([sample_at::(1)]), + ]); + let outcome = collector.collect_signatures().await; + assert!(!outcome.successful()); + let signatures = outcome.all_signatures(); + assert!(signatures.is_empty()); + } + + async fn lazy_always_skip_user_single_tx_e2< + E: IsEntity + 'static, + >() { + let collector = SignaturesCollector::test_lazy_always_skip([ + TXToSign::sample([sample_at::(2)]), + ]); + let outcome = collector.collect_signatures().await; + assert!(!outcome.successful()); + let signatures = outcome.all_signatures(); + assert!(signatures.is_empty()); + } + + async fn lazy_always_skip_user_e3() { + let collector = SignaturesCollector::test_lazy_always_skip([ + TXToSign::sample([sample_at::(3)]), + ]); + let outcome = collector.collect_signatures().await; + assert!(!outcome.successful()); + let signatures = outcome.all_signatures(); + assert!(signatures.is_empty()); + } + + async fn lazy_always_skip_user_e4() { + let collector = SignaturesCollector::test_lazy_always_skip([ + TXToSign::sample([sample_at::(4)]), + ]); + let outcome = collector.collect_signatures().await; + assert!(!outcome.successful()); + let signatures = outcome.all_signatures(); + assert!(signatures.is_empty()); + } + + async fn lazy_always_skip_user_e5() { + let collector = SignaturesCollector::test_lazy_always_skip([ + TXToSign::sample([sample_at::(5)]), + ]); + let outcome = collector.collect_signatures().await; + assert!(!outcome.successful()); + let signatures = outcome.all_signatures(); + assert!(signatures.is_empty()); + } + + async fn lazy_always_skip_user_e6() { + let collector = SignaturesCollector::test_lazy_always_skip([ + TXToSign::sample([sample_at::(6)]), + ]); + let outcome = collector.collect_signatures().await; + assert!(!outcome.successful()); + let signatures = outcome.all_signatures(); + assert!(signatures.is_empty()); + } + + async fn lazy_always_skip_user_e7() { + let collector = SignaturesCollector::test_lazy_always_skip([ + TXToSign::sample([sample_at::(7)]), + ]); + let outcome = collector.collect_signatures().await; + assert!(!outcome.successful()); + let signatures = outcome.all_signatures(); + assert!(signatures.is_empty()); + } + + async fn failure_e0() { + let collector = SignaturesCollector::test_prudent_with_failures( + [TXToSign::sample([sample_at::(0)])], + SimulatedFailures::with_simulated_failures([ + FactorSourceIDFromHash::sample_at(0), + ]), + ); + let outcome = collector.collect_signatures().await; + assert!(!outcome.successful()); + assert_eq!( + outcome + .ids_of_neglected_factor_sources_failed() + .into_iter() + .collect_vec(), + vec![FactorSourceIDFromHash::sample_at(0)] + ); + assert!(outcome + .ids_of_neglected_factor_sources_skipped_by_user() + .is_empty()) + } + + async fn failure_e5() { + let collector = SignaturesCollector::new_test( + SigningFinishEarlyStrategy::r#continue(), + FactorSource::sample_all(), + [TXToSign::sample([sample_at::(5)])], + SimulatedUser::prudent_with_failures( + SimulatedFailures::with_simulated_failures([ + FactorSourceIDFromHash::sample_at(4), + ]), + ), + RoleKind::Primary, + ); + + let outcome = collector.collect_signatures().await; + assert!(outcome.successful()); + assert_eq!( + outcome + .ids_of_neglected_factor_sources_failed() + .into_iter() + .collect_vec(), + vec![FactorSourceIDFromHash::sample_at(4)] + ); + assert!(outcome + .ids_of_neglected_factor_sources_skipped_by_user() + .is_empty()); + } + + async fn building_can_succeed_even_if_one_factor_source_fails_assert_ids_of_successful_tx_e4< + E: IsEntity + 'static, + >() { + let collector = SignaturesCollector::test_prudent_with_failures( + [TXToSign::sample([sample_at::(4)])], + SimulatedFailures::with_simulated_failures([ + FactorSourceIDFromHash::sample_at(3), + ]), + ); + let outcome = collector.collect_signatures().await; + assert!(outcome.successful()); + assert_eq!( + outcome + .signatures_of_successful_transactions() + .into_iter() + .map(|f| f.factor_source_id()) + .collect::>(), + IndexSet::<_>::from_iter([ + FactorSourceIDFromHash::sample_at(0), + FactorSourceIDFromHash::sample_at(5) + ]) + ); + } + + async fn building_can_succeed_even_if_one_factor_source_fails_assert_ids_of_failed_tx_e4< + E: IsEntity + 'static, + >() { + let collector = SignaturesCollector::test_prudent_with_failures( + [TXToSign::sample([sample_at::(4)])], + SimulatedFailures::with_simulated_failures([ + FactorSourceIDFromHash::sample_at(3), + ]), + ); + let outcome = collector.collect_signatures().await; + assert!(outcome.successful()); + assert_eq!( + outcome.ids_of_neglected_factor_sources(), + IndexSet::<_>::just(FactorSourceIDFromHash::sample_at(3)) + ); + } + + mod account { + use super::*; + type E = Account; + + #[actix_rt::test] + async fn prudent_user_single_tx_a0() { + prudent_user_single_tx_e0::().await + } + + #[actix_rt::test] + async fn prudent_user_single_tx_a0_assert_correct_intent_hash_is_signed( + ) { + prudent_user_single_tx_e0_assert_correct_intent_hash_is_signed::().await + } + + #[actix_rt::test] + async fn prudent_user_single_tx_a0_assert_correct_owner_has_signed( + ) { + prudent_user_single_tx_e0_assert_correct_owner_has_signed::().await + } + + #[actix_rt::test] + async fn prudent_user_single_tx_a0_assert_correct_owner_factor_instance_signed( + ) { + prudent_user_single_tx_e0_assert_correct_owner_factor_instance_signed::() + .await + } + + #[actix_rt::test] + async fn prudent_user_single_tx_a1() { + prudent_user_single_tx_e1::().await + } + + #[actix_rt::test] + async fn prudent_user_single_tx_a2() { + prudent_user_single_tx_e2::().await + } + + #[actix_rt::test] + async fn prudent_user_single_tx_a3() { + prudent_user_single_tx_e3::().await + } + + #[actix_rt::test] + async fn prudent_user_single_tx_a4() { + prudent_user_single_tx_e4::().await + } + + #[actix_rt::test] + async fn prudent_user_single_tx_a5() { + prudent_user_single_tx_e5::().await + } + + #[actix_rt::test] + async fn prudent_user_single_tx_a6() { + prudent_user_single_tx_e6::().await + } + + #[actix_rt::test] + async fn prudent_user_single_tx_a7() { + prudent_user_single_tx_e7::().await + } + + #[actix_rt::test] + async fn lazy_sign_minimum_user_single_tx_a0() { + lazy_sign_minimum_user_single_tx_e0::().await + } + + #[actix_rt::test] + async fn lazy_sign_minimum_user_single_tx_a1() { + lazy_sign_minimum_user_single_tx_e1::().await + } + + #[actix_rt::test] + async fn lazy_sign_minimum_user_single_tx_a2() { + lazy_sign_minimum_user_single_tx_e2::().await + } + + #[actix_rt::test] + async fn lazy_sign_minimum_user_a3() { + lazy_sign_minimum_user_e3::().await + } + + #[actix_rt::test] + async fn lazy_sign_minimum_user_a4() { + lazy_sign_minimum_user_e4::().await + } + + #[actix_rt::test] + async fn lazy_sign_minimum_user_a5() { + lazy_sign_minimum_user_e5::().await + } + + #[actix_rt::test] + async fn lazy_sign_minimum_user_a6() { + lazy_sign_minimum_user_e6::().await + } + + #[actix_rt::test] + async fn lazy_sign_minimum_user_a7() { + lazy_sign_minimum_user_e7::().await + } + + #[actix_rt::test] + async fn lazy_sign_minimum_user_a5_last_factor_used() { + lazy_sign_minimum_user_e5_last_factor_used::().await + } + + #[actix_rt::test] + async fn lazy_sign_minimum_all_known_factors_used_as_override_factors_signed_with_device_for_account( + ) { + lazy_sign_minimum_all_known_factors_used_as_override_factors_signed_with_device_for_entity::().await + } + + #[actix_rt::test] + async fn lazy_always_skip_user_single_tx_a0() { + lazy_always_skip_user_single_tx_e0::().await + } + + #[actix_rt::test] + async fn fail_get_skipped_a0() { + fail_get_neglected_e0::().await + } + + #[actix_rt::test] + async fn lazy_always_skip_user_single_tx_a1() { + lazy_always_skip_user_single_tx_e1::().await + } + + #[actix_rt::test] + async fn lazy_always_skip_user_single_tx_a2() { + lazy_always_skip_user_single_tx_e2::().await + } + + #[actix_rt::test] + async fn lazy_always_skip_user_a3() { + lazy_always_skip_user_e3::().await + } + + #[actix_rt::test] + async fn lazy_always_skip_user_a4() { + lazy_always_skip_user_e4::().await + } + + #[actix_rt::test] + async fn lazy_always_skip_user_a5() { + lazy_always_skip_user_e5::().await + } + + #[actix_rt::test] + async fn lazy_always_skip_user_a6() { + lazy_always_skip_user_e6::().await + } + + #[actix_rt::test] + async fn lazy_always_skip_user_a7() { + lazy_always_skip_user_e7::().await + } + + #[actix_rt::test] + async fn failure_a0() { + failure_e0::().await + } + + #[actix_rt::test] + async fn failure_a5() { + failure_e5::().await + } + + #[actix_rt::test] + async fn building_can_succeed_even_if_one_factor_source_fails_assert_ids_of_successful_tx( + ) { + building_can_succeed_even_if_one_factor_source_fails_assert_ids_of_successful_tx_e4::() + .await + } + + #[actix_rt::test] + async fn building_can_succeed_even_if_one_factor_source_fails_assert_ids_of_failed_tx( + ) { + building_can_succeed_even_if_one_factor_source_fails_assert_ids_of_failed_tx_e4::().await + } + } + + mod persona { + use super::*; + type E = Persona; + + #[actix_rt::test] + async fn prudent_user_single_tx_p0() { + prudent_user_single_tx_e0::().await + } + + #[actix_rt::test] + async fn prudent_user_single_tx_p0_assert_correct_intent_hash_is_signed( + ) { + prudent_user_single_tx_e0_assert_correct_intent_hash_is_signed::().await + } + + #[actix_rt::test] + async fn prudent_user_single_tx_p0_assert_correct_owner_has_signed( + ) { + prudent_user_single_tx_e0_assert_correct_owner_has_signed::().await + } + + #[actix_rt::test] + async fn prudent_user_single_tx_p0_assert_correct_owner_factor_instance_signed( + ) { + prudent_user_single_tx_e0_assert_correct_owner_factor_instance_signed::() + .await + } + + #[actix_rt::test] + async fn prudent_user_single_tx_p1() { + prudent_user_single_tx_e1::().await + } + + #[actix_rt::test] + async fn prudent_user_single_tx_p2() { + prudent_user_single_tx_e2::().await + } + + #[actix_rt::test] + async fn prudent_user_single_tx_p3() { + prudent_user_single_tx_e3::().await + } + + #[actix_rt::test] + async fn prudent_user_single_tx_p4() { + prudent_user_single_tx_e4::().await + } + + #[actix_rt::test] + async fn prudent_user_single_tx_p5() { + prudent_user_single_tx_e5::().await + } + + #[actix_rt::test] + async fn prudent_user_single_tx_p6() { + prudent_user_single_tx_e6::().await + } + + #[actix_rt::test] + async fn prudent_user_single_tx_p7() { + prudent_user_single_tx_e7::().await + } + + #[actix_rt::test] + async fn lazy_sign_minimum_user_single_tx_p0() { + lazy_sign_minimum_user_single_tx_e0::().await + } + + #[actix_rt::test] + async fn lazy_sign_minimum_user_single_tx_p1() { + lazy_sign_minimum_user_single_tx_e1::().await + } + + #[actix_rt::test] + async fn lazy_sign_minimum_user_single_tx_p2() { + lazy_sign_minimum_user_single_tx_e2::().await + } + + #[actix_rt::test] + async fn lazy_sign_minimum_user_p3() { + lazy_sign_minimum_user_e3::().await + } + + #[actix_rt::test] + async fn lazy_sign_minimum_user_p4() { + lazy_sign_minimum_user_e4::().await + } + + #[actix_rt::test] + async fn lazy_sign_minimum_user_p5() { + lazy_sign_minimum_user_e5::().await + } + + #[actix_rt::test] + async fn lazy_sign_minimum_user_p6() { + lazy_sign_minimum_user_e6::().await + } + + #[actix_rt::test] + async fn lazy_sign_minimum_user_p7() { + lazy_sign_minimum_user_e7::().await + } + + #[actix_rt::test] + async fn lazy_sign_minimum_user_p5_last_factor_used() { + lazy_sign_minimum_user_e5_last_factor_used::().await + } + + #[actix_rt::test] + async fn lazy_sign_minimum_all_known_factors_used_as_override_factors_signed_with_device_for_account( + ) { + lazy_sign_minimum_all_known_factors_used_as_override_factors_signed_with_device_for_entity::().await + } + + #[actix_rt::test] + async fn lazy_always_skip_user_single_tx_p0() { + lazy_always_skip_user_single_tx_e0::().await + } + + #[actix_rt::test] + async fn fail_get_skipped_p0() { + fail_get_neglected_e0::().await + } + + #[actix_rt::test] + async fn lazy_always_skip_user_single_tx_p1() { + lazy_always_skip_user_single_tx_e1::().await + } + + #[actix_rt::test] + async fn lazy_always_skip_user_single_tx_p2() { + lazy_always_skip_user_single_tx_e2::().await + } + + #[actix_rt::test] + async fn lazy_always_skip_user_p3() { + lazy_always_skip_user_e3::().await + } + + #[actix_rt::test] + async fn lazy_always_skip_user_p4() { + lazy_always_skip_user_e4::().await + } + + #[actix_rt::test] + async fn lazy_always_skip_user_p5() { + lazy_always_skip_user_e5::().await + } + + #[actix_rt::test] + async fn lazy_always_skip_user_p6() { + lazy_always_skip_user_e6::().await + } + + #[actix_rt::test] + async fn lazy_always_skip_user_p7() { + lazy_always_skip_user_e7::().await + } + + #[actix_rt::test] + async fn failure_p0() { + failure_e0::().await + } + + #[actix_rt::test] + async fn failure_p5() { + failure_e5::().await + } + + #[actix_rt::test] + async fn building_can_succeed_even_if_one_factor_source_fails_assert_ids_of_successful_tx( + ) { + building_can_succeed_even_if_one_factor_source_fails_assert_ids_of_successful_tx_e4::() + .await + } + + #[actix_rt::test] + async fn building_can_succeed_even_if_one_factor_source_fails_assert_ids_of_failed_tx( + ) { + building_can_succeed_even_if_one_factor_source_fails_assert_ids_of_failed_tx_e4::().await + } + } + } + } +} diff --git a/crates/sargon/src/signing/collector/signatures_collector_dependencies.rs b/crates/sargon/src/signing/collector/signatures_collector_dependencies.rs new file mode 100644 index 000000000..0bfa193be --- /dev/null +++ b/crates/sargon/src/signing/collector/signatures_collector_dependencies.rs @@ -0,0 +1,36 @@ +use crate::prelude::*; + +pub(super) struct SignaturesCollectorDependencies { + /// If `true` we stop collecting signatures as soon as all transactions are + /// valid. This is typically always set to `true`, but can be useful for + /// tests to set to `false` to see how the system behaves. + pub(super) finish_early_strategy: SigningFinishEarlyStrategy, + + /// A collection of "interactors" used to sign with factor sources. + pub(super) interactors: Arc, + + /// Factor sources grouped by kind, sorted according to "friction order", + /// that is, we want to control which FactorSourceKind users sign with + /// first, second etc, e.g. typically we prompt user to sign with Ledgers + /// first, and if a user might lack access to that Ledger device, then it is + /// best to "fail fast", otherwise we might waste the users time, if she has + /// e.g. answered security questions and then is asked to use a Ledger + /// she might not have handy at the moment - or might not be in front of a + /// computer and thus unable to make a connection between the Radix Wallet + /// and a Ledger device. + pub(super) factors_of_kind: IndexSet, +} + +impl SignaturesCollectorDependencies { + pub(crate) fn new( + finish_early_strategy: SigningFinishEarlyStrategy, + interactors: Arc, + factors_of_kind: IndexSet, + ) -> Self { + Self { + finish_early_strategy, + interactors, + factors_of_kind, + } + } +} diff --git a/crates/sargon/src/signing/collector/signatures_collector_preprocessor.rs b/crates/sargon/src/signing/collector/signatures_collector_preprocessor.rs new file mode 100644 index 000000000..c1b2ea1c7 --- /dev/null +++ b/crates/sargon/src/signing/collector/signatures_collector_preprocessor.rs @@ -0,0 +1,116 @@ +use crate::prelude::*; + +pub(crate) struct SignaturesCollectorPreprocessor { + transactions: IndexSet, +} + +pub(crate) fn sort_group_factors( + used_factor_sources: HashSet, +) -> IndexSet { + let factors_of_kind: HashMap> = + used_factor_sources + .into_iter() + .into_grouping_map_by(|x| x.factor_source_kind()) + .collect::>(); + + let mut factors_of_kind = factors_of_kind + .into_iter() + .map(|(k, v)| (k, v.into_iter().sorted().collect::>())) + .collect::>>(); + + factors_of_kind.sort_keys(); + + factors_of_kind + .into_iter() + .map(|(k, v)| { + FactorSourcesOfKind::new(k, v) + .expect("All factors should be of the same kind, since this is calling iter on a Map, using kind as key. Did you just move around lines of code?") + }) + .collect::>() +} + +impl SignaturesCollectorPreprocessor { + pub(super) fn new(transactions: IndexSet) -> Self { + Self { transactions } + } + + pub(super) fn preprocess( + self, + profile_factor_sources: IndexSet, + role_kind: RoleKind, + ) -> (Petitions, IndexSet) { + let transactions = self.transactions; + let mut petitions_for_all_transactions = + IndexMap::::new(); + + // We care for only the factor sources which are HD based + let mut all_factor_sources_in_profile = + HashMap::::new(); + profile_factor_sources.into_iter().for_each(|f| { + if let Some(id) = f.factor_source_id().as_hash() { + all_factor_sources_in_profile.insert(*id, f); + } + }); + + let mut factor_to_payloads = + HashMap::>::new(); + + let mut used_factor_sources = HashSet::::new(); + + let mut register_factor_in_tx = + |id: &FactorSourceIDFromHash, txid: &IntentHash| { + if let Some(ref mut txids) = factor_to_payloads.get_mut(id) { + txids.insert(txid.clone()); + } else { + factor_to_payloads + .insert(*id, IndexSet::just(txid.clone())); + } + + assert!(!factor_to_payloads.is_empty()); + + let factor_source = all_factor_sources_in_profile + .get(id) + .expect("Should have all factor sources"); + used_factor_sources.insert(factor_source.clone()); + + assert!(!used_factor_sources.is_empty()); + }; + + for transaction in transactions.into_iter() { + let mut petitions_for_entities = + HashMap::::new(); + + for entity in transaction.entities_requiring_auth() { + let address = entity.address(); + let petition = PetitionForEntity::new_from_entity( + transaction.intent_hash.clone(), + entity, + role_kind, + ); + + petition.all_factor_instances().iter().for_each(|f| { + register_factor_in_tx( + &f.factor_source_id(), + &transaction.intent_hash, + ) + }); + petitions_for_entities.insert(address, petition); + } + + let petition_of_tx = PetitionForTransaction::new( + transaction.intent_hash.clone(), + petitions_for_entities, + ); + + petitions_for_all_transactions + .insert(transaction.intent_hash, petition_of_tx); + } + + let factors_of_kind = sort_group_factors(used_factor_sources); + + let petitions = + Petitions::new(factor_to_payloads, petitions_for_all_transactions); + + (petitions, factors_of_kind) + } +} diff --git a/crates/sargon/src/signing/collector/signatures_collector_state.rs b/crates/sargon/src/signing/collector/signatures_collector_state.rs new file mode 100644 index 000000000..659094c08 --- /dev/null +++ b/crates/sargon/src/signing/collector/signatures_collector_state.rs @@ -0,0 +1,14 @@ +use crate::prelude::*; + +#[derive(derive_more::Debug)] +#[debug("{:#?}", petitions.borrow())] +pub(super) struct SignaturesCollectorState { + pub(super) petitions: RefCell, +} +impl SignaturesCollectorState { + pub(crate) fn new(petitions: Petitions) -> Self { + Self { + petitions: RefCell::new(petitions), + } + } +} diff --git a/crates/sargon/src/signing/collector/signing_finish_early_strategy.rs b/crates/sargon/src/signing/collector/signing_finish_early_strategy.rs new file mode 100644 index 000000000..1a8c4ed29 --- /dev/null +++ b/crates/sargon/src/signing/collector/signing_finish_early_strategy.rs @@ -0,0 +1,121 @@ +use crate::prelude::*; + +/// Describes what `SignaturesCollector` should do when all transactions are valid. It can either +/// finish execution when `SignaturesCollectingContinuation::FinishEarly` or continue collecting +/// signatures when it is of `SignaturesCollectingContinuation::Continue`. +/// +/// The default behavior is to finish early when all needed signatures are provided. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct WhenAllTransactionsAreValid(pub SignaturesCollectingContinuation); + +impl WhenAllTransactionsAreValid { + pub fn finish_early() -> Self { + Self(SignaturesCollectingContinuation::FinishEarly) + } + pub fn r#continue() -> Self { + Self(SignaturesCollectingContinuation::Continue) + } +} + +impl Default for WhenAllTransactionsAreValid { + fn default() -> Self { + Self::finish_early() + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct WhenSomeTransactionIsInvalid(pub SignaturesCollectingContinuation); + +impl WhenSomeTransactionIsInvalid { + pub fn finish_early() -> Self { + Self(SignaturesCollectingContinuation::FinishEarly) + } + pub fn r#continue() -> Self { + Self(SignaturesCollectingContinuation::Continue) + } +} + +impl Default for WhenSomeTransactionIsInvalid { + fn default() -> Self { + Self::r#continue() + } +} + +/// Strategy to use for finishing early, i.e. stop collecting more signatures +#[derive(Clone, Default, Copy, Debug, PartialEq, Eq)] +pub struct SigningFinishEarlyStrategy { + pub(crate) when_all_transactions_are_valid: WhenAllTransactionsAreValid, + pub(crate) when_some_transaction_is_invalid: WhenSomeTransactionIsInvalid, +} +impl SigningFinishEarlyStrategy { + pub fn new( + when_all_transactions_are_valid: WhenAllTransactionsAreValid, + when_some_transaction_is_invalid: WhenSomeTransactionIsInvalid, + ) -> Self { + Self { + when_all_transactions_are_valid, + when_some_transaction_is_invalid, + } + } + + #[allow(unused)] + pub(crate) fn r#continue() -> Self { + Self::new( + WhenAllTransactionsAreValid::r#continue(), + WhenSomeTransactionIsInvalid::r#continue(), + ) + } + + #[allow(unused)] + pub(crate) fn finish_early() -> Self { + Self::new( + WhenAllTransactionsAreValid::finish_early(), + WhenSomeTransactionIsInvalid::finish_early(), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + type Sut = SigningFinishEarlyStrategy; + + #[test] + fn test_continue() { + let sut = Sut::r#continue(); + assert_eq!( + sut.when_all_transactions_are_valid.0, + SignaturesCollectingContinuation::Continue + ); + assert_eq!( + sut.when_some_transaction_is_invalid.0, + SignaturesCollectingContinuation::Continue + ); + } + + #[test] + fn test_finish_early() { + let sut = Sut::finish_early(); + assert_eq!( + sut.when_all_transactions_are_valid.0, + SignaturesCollectingContinuation::FinishEarly + ); + assert_eq!( + sut.when_some_transaction_is_invalid.0, + SignaturesCollectingContinuation::FinishEarly + ); + } + + #[test] + fn test_default_is_finish_when_valid_continue_if_invalid() { + let sut = Sut::default(); + assert_eq!( + sut.when_all_transactions_are_valid.0, + SignaturesCollectingContinuation::FinishEarly + ); + assert_eq!( + sut.when_some_transaction_is_invalid.0, + SignaturesCollectingContinuation::Continue + ); + } +} diff --git a/crates/sargon/src/signing/host_interaction/interactors/mod.rs b/crates/sargon/src/signing/host_interaction/interactors/mod.rs new file mode 100644 index 000000000..dc69c7aff --- /dev/null +++ b/crates/sargon/src/signing/host_interaction/interactors/mod.rs @@ -0,0 +1,7 @@ +mod mono_factor_sign_interactor; +mod poly_factor_sign_interactor; +mod sign_interactor; + +pub use mono_factor_sign_interactor::*; +pub use poly_factor_sign_interactor::*; +pub use sign_interactor::*; diff --git a/crates/sargon/src/signing/host_interaction/interactors/mono_factor_sign_interactor.rs b/crates/sargon/src/signing/host_interaction/interactors/mono_factor_sign_interactor.rs new file mode 100644 index 000000000..d9c2819bf --- /dev/null +++ b/crates/sargon/src/signing/host_interaction/interactors/mono_factor_sign_interactor.rs @@ -0,0 +1,23 @@ +use crate::prelude::*; + +/// An interactor for a factor source kind which supports performing +/// *Batch* signing *serially*. +/// +/// Meaning we initiate and prompt user for signing with one factor source +/// at a time, where each signing operation is support batch signing, that is +/// signing multiple transactions each with multiple keys (derivations paths). +/// +/// The user might choose to SKIP the current factor source, and move on to the +/// next one. +/// +/// Example of a MonoFactor Batch Signing Driver is SecurityQuestionsFactorSource, +/// where it does not make any sense to let user in poly answer multiple +/// questions from different security questions factor sources (in fact we +/// might not even allow multiple SecurityQuestionsFactorSources to be used). +#[async_trait::async_trait] +pub trait MonoFactorSignInteractor { + async fn sign( + &self, + request: MonoFactorSignRequest, + ) -> SignWithFactorsOutcome; +} diff --git a/crates/sargon/src/signing/host_interaction/interactors/poly_factor_sign_interactor.rs b/crates/sargon/src/signing/host_interaction/interactors/poly_factor_sign_interactor.rs new file mode 100644 index 000000000..8517cd6a3 --- /dev/null +++ b/crates/sargon/src/signing/host_interaction/interactors/poly_factor_sign_interactor.rs @@ -0,0 +1,27 @@ +use crate::prelude::*; + +/// An interactor for a factor source kind which supports *Batch* usage of +/// multiple factor sources in poly. +/// +/// Most FactorSourceKinds does in fact NOT support poly usage, +/// e.g. signing using multiple factors sources at once, but some do, +/// typically the DeviceFactorSource does, i.e. we can load multiple +/// mnemonics from secure storage in one go and sign with all of them +/// "in poly". +/// +/// This is a bit of a misnomer, as we don't actually use them in poly, +/// but rather we iterate through all mnemonics and derive public keys/ +/// or sign a payload with each of them in sequence +/// +/// The user does not have the ability to SKIP a certain factor source, +/// instead either ALL factor sources are used to sign the transactions +/// or none. +/// +/// Example of a PolyFactor Batch Signing Driver is that for DeviceFactorSource. +#[async_trait::async_trait] +pub trait PolyFactorSignInteractor { + async fn sign( + &self, + request: PolyFactorSignRequest, + ) -> SignWithFactorsOutcome; +} diff --git a/crates/sargon/src/signing/host_interaction/interactors/sign_interactor.rs b/crates/sargon/src/signing/host_interaction/interactors/sign_interactor.rs new file mode 100644 index 000000000..737c5baf9 --- /dev/null +++ b/crates/sargon/src/signing/host_interaction/interactors/sign_interactor.rs @@ -0,0 +1,19 @@ +use crate::prelude::*; + +/// An interactor which can sign transactions - either in poly or mono. +pub enum SignInteractor { + PolyFactor(Arc), + MonoFactor(Arc), +} + +impl SignInteractor { + #[allow(unused)] + pub fn poly(interactor: Arc) -> Self { + Self::PolyFactor(interactor) + } + + #[allow(unused)] + pub fn mono(interactor: Arc) -> Self { + Self::MonoFactor(interactor) + } +} diff --git a/crates/sargon/src/signing/host_interaction/mod.rs b/crates/sargon/src/signing/host_interaction/mod.rs new file mode 100644 index 000000000..118e95b49 --- /dev/null +++ b/crates/sargon/src/signing/host_interaction/mod.rs @@ -0,0 +1,9 @@ +mod interactors; +mod requests; +mod sign_interactors; +mod sign_response; + +pub use interactors::*; +pub use requests::*; +pub use sign_interactors::*; +pub use sign_response::*; diff --git a/crates/sargon/src/signing/host_interaction/requests/mod.rs b/crates/sargon/src/signing/host_interaction/requests/mod.rs new file mode 100644 index 000000000..50ab8a663 --- /dev/null +++ b/crates/sargon/src/signing/host_interaction/requests/mod.rs @@ -0,0 +1,9 @@ +mod mono_factor_sign_request; +mod mono_factor_sign_request_input; +mod poly_factor_sign_request; +mod transaction_sign_request_input; + +pub use mono_factor_sign_request::*; +pub use mono_factor_sign_request_input::*; +pub use poly_factor_sign_request::*; +pub use transaction_sign_request_input::*; diff --git a/crates/sargon/src/signing/host_interaction/requests/mono_factor_sign_request.rs b/crates/sargon/src/signing/host_interaction/requests/mono_factor_sign_request.rs new file mode 100644 index 000000000..b239e11af --- /dev/null +++ b/crates/sargon/src/signing/host_interaction/requests/mono_factor_sign_request.rs @@ -0,0 +1,33 @@ +use crate::prelude::*; + +/// A request to sign a batch of transactions with a single factor source. +#[derive(derive_more::Debug, Clone)] +#[debug("input: {:#?}", input)] +pub struct MonoFactorSignRequest { + /// The input needed to sign the transactions. + pub input: MonoFactorSignRequestInput, + + /// A collection of transactions which would be invalid if the user skips + /// signing with this factor source, or if we fail to sign + pub invalid_transactions_if_neglected: + IndexSet, +} + +impl MonoFactorSignRequest { + pub fn new( + input: MonoFactorSignRequestInput, + invalid_transactions_if_neglected: IndexSet< + InvalidTransactionIfNeglected, + >, + ) -> Self { + Self { + input, + invalid_transactions_if_neglected, + } + } + + #[allow(unused)] + pub(crate) fn factor_source_kind(&self) -> FactorSourceKind { + self.input.factor_source_kind() + } +} diff --git a/crates/sargon/src/signing/host_interaction/requests/mono_factor_sign_request_input.rs b/crates/sargon/src/signing/host_interaction/requests/mono_factor_sign_request_input.rs new file mode 100644 index 000000000..7daf1cfb5 --- /dev/null +++ b/crates/sargon/src/signing/host_interaction/requests/mono_factor_sign_request_input.rs @@ -0,0 +1,94 @@ +use crate::prelude::*; + +/// A batch of transactions each batching over multiple keys (derivation paths) +/// to sign each transaction with. +#[derive(Clone, Debug, PartialEq, Eq, std::hash::Hash)] +pub struct MonoFactorSignRequestInput { + /// The ID of the factor source used to sign each per_transaction + pub factor_source_id: FactorSourceIDFromHash, + + // The `factor_source_id` of each item must match `self.factor_source_id`. + pub per_transaction: Vec, +} + +impl MonoFactorSignRequestInput { + /// # Panics + /// Panics if `per_transaction` is empty + /// + /// Also panics if `per_transaction` if the factor source id + /// of each request does not match `factor_source_id`. + pub(crate) fn new( + factor_source_id: FactorSourceIDFromHash, + per_transaction: IndexSet, + ) -> Self { + assert!( + !per_transaction.is_empty(), + "Invalid input. No transaction to sign, this is a programmer error." + ); + + assert!(per_transaction + .iter() + .all(|f| f.factor_source_id == factor_source_id), "Discprepancy! Input for one of the transactions has a mismatching FactorSourceID, this is a programmer error."); + + Self { + factor_source_id, + per_transaction: per_transaction.into_iter().collect(), + } + } + + /// Returns the factor source kind of the factor source id. + #[allow(unused)] + pub(crate) fn factor_source_kind(&self) -> FactorSourceKind { + self.factor_source_id.kind + } +} + +impl HasSampleValues for MonoFactorSignRequestInput { + /// Creates a new MonoFactorSignRequestInput with sample values. + fn sample() -> Self { + let input = TransactionSignRequestInput::sample(); + Self::new(input.clone().factor_source_id, IndexSet::just(input)) + } + + /// Creates a new MonoFactorSignRequestInput with sample values. + fn sample_other() -> Self { + let input = TransactionSignRequestInput::sample_other(); + Self::new(input.clone().factor_source_id, IndexSet::just(input)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + type Sut = MonoFactorSignRequestInput; + + #[test] + fn equality() { + assert_eq!(Sut::sample(), Sut::sample()); + assert_eq!(Sut::sample_other(), Sut::sample_other()); + } + + #[test] + fn inequality() { + assert_ne!(Sut::sample(), Sut::sample_other()); + } + + #[test] + #[should_panic( + expected = "Invalid input. No transaction to sign, this is a programmer error." + )] + fn panics_if_per_transaction_is_empty() { + Sut::new(FactorSourceIDFromHash::sample(), IndexSet::new()); + } + + #[test] + #[should_panic( + expected = "Discprepancy! Input for one of the transactions has a mismatching FactorSourceID, this is a programmer error." + )] + fn panics_if_factor_source_mismatch() { + Sut::new( + FactorSourceIDFromHash::sample_other(), + IndexSet::just(TransactionSignRequestInput::sample_other()), + ); + } +} diff --git a/crates/sargon/src/signing/host_interaction/requests/poly_factor_sign_request.rs b/crates/sargon/src/signing/host_interaction/requests/poly_factor_sign_request.rs new file mode 100644 index 000000000..4b2bd2cb6 --- /dev/null +++ b/crates/sargon/src/signing/host_interaction/requests/poly_factor_sign_request.rs @@ -0,0 +1,91 @@ +use crate::prelude::*; + +/// A collection of **many** factor sources to use to sign, transactions with multiple keys +/// (derivations paths). +#[derive(derive_more::Debug, Clone)] +#[debug("per_factor_source: {:#?}", per_factor_source)] +pub struct PolyFactorSignRequest { + factor_source_kind: FactorSourceKind, + + /// Per factor source, a set of transactions to sign, with + /// multiple derivations paths. + pub per_factor_source: + IndexMap, + + /// A collection of transactions which would be invalid if the user skips + /// signing with this factor source. + pub invalid_transactions_if_neglected: + IndexSet, +} + +impl PolyFactorSignRequest { + /// # Panics + /// Panics if `per_factor_source` is empty + /// + /// Panics if not all factor sources are of the same kind + pub(crate) fn new( + factor_source_kind: FactorSourceKind, + per_factor_source: IndexMap< + FactorSourceIDFromHash, + MonoFactorSignRequestInput, + >, + invalid_transactions_if_neglected: IndexSet< + InvalidTransactionIfNeglected, + >, + ) -> Self { + assert!( + !per_factor_source.is_empty(), + "Invalid input, per_factor_source must not be empty, this is a programmer error." + ); + assert!( + per_factor_source + .values() + .all(|f| f.factor_source_id.kind == factor_source_kind), + "Discrepancy! All factor sources must be of the same kind, this is a programmer error." + ); + + Self { + factor_source_kind, + per_factor_source, + invalid_transactions_if_neglected, + } + } + + pub fn factor_source_ids(&self) -> IndexSet { + self.per_factor_source.keys().cloned().collect() + } + + #[allow(unused)] + pub(crate) fn factor_source_kind(&self) -> FactorSourceKind { + self.factor_source_kind + } +} + +#[cfg(test)] +mod tests { + use super::*; + type Sut = PolyFactorSignRequest; + + #[test] + #[should_panic( + expected = "Invalid input, per_factor_source must not be empty, this is a programmer error." + )] + fn panics_if_per_factor_source_is_empty() { + Sut::new(FactorSourceKind::Device, IndexMap::new(), IndexSet::new()); + } + + #[test] + #[should_panic( + expected = "Discrepancy! All factor sources must be of the same kind, this is a programmer error." + )] + fn panics_if_wrong_factor_source_kind() { + Sut::new( + FactorSourceKind::ArculusCard, + IndexMap::just(( + FactorSourceIDFromHash::sample(), + MonoFactorSignRequestInput::sample(), + )), + IndexSet::new(), + ); + } +} diff --git a/crates/sargon/src/signing/host_interaction/requests/transaction_sign_request_input.rs b/crates/sargon/src/signing/host_interaction/requests/transaction_sign_request_input.rs new file mode 100644 index 000000000..8bcddb212 --- /dev/null +++ b/crates/sargon/src/signing/host_interaction/requests/transaction_sign_request_input.rs @@ -0,0 +1,117 @@ +use crate::prelude::*; + +/// A batch of keys (derivation paths) all being factor instances of a HDFactorSource +/// with id `factor_source_id` to sign a single transaction with, which hash +/// is `intent_hash`. +#[derive(PartialEq, Eq, Clone, Debug, Hash)] +pub struct TransactionSignRequestInput { + /// Hash to sign + intent_hash: IntentHash, + + /// ID of factor to use to sign + pub(crate) factor_source_id: FactorSourceIDFromHash, + + /// The derivation paths to use to derive the private keys to sign with. The + /// `factor_source_id` of each item must match `factor_source_id`. + owned_factor_instances: Vec, +} + +impl TransactionSignRequestInput { + /// # Panics + /// Panics if any of the owned factor instances does not match the `factor_source_id`. + /// + /// Panics if `owned_factor_instances` is empty. + pub(crate) fn new( + intent_hash: IntentHash, + factor_source_id: FactorSourceIDFromHash, + owned_factor_instances: IndexSet, + ) -> Self { + assert!( + !owned_factor_instances.is_empty(), + "Invalid input, `owned_factor_instances` must not be empty." + ); + assert!(owned_factor_instances + .iter() + .all(|f| f.by_factor_source(factor_source_id)), "Discrepancy! Mismatch between FactorSourceID of owned factor instances and specified FactorSourceID, this is a programmer error."); + Self { + intent_hash, + factor_source_id, + owned_factor_instances: owned_factor_instances + .into_iter() + .collect_vec(), + } + } + + #[allow(unused)] + pub fn signature_inputs(&self) -> IndexSet { + self.owned_factor_instances + .clone() + .into_iter() + .map(|fi| HDSignatureInput::new(self.intent_hash.clone(), fi)) + .collect() + } +} + +impl HasSampleValues for TransactionSignRequestInput { + fn sample() -> Self { + let owned_factor_instance = OwnedFactorInstance::sample(); + let factor_source_id = &owned_factor_instance.factor_source_id(); + Self::new( + IntentHash::sample(), + *factor_source_id, + IndexSet::just(owned_factor_instance), + ) + } + + fn sample_other() -> Self { + let owned_factor_instance = OwnedFactorInstance::sample_other(); + let factor_source_id = &owned_factor_instance.factor_source_id(); + Self::new( + IntentHash::sample_other(), + *factor_source_id, + IndexSet::just(owned_factor_instance), + ) + } +} + +#[cfg(test)] +mod tests_batch_req { + use super::*; + + type Sut = TransactionSignRequestInput; + + #[test] + fn equality() { + assert_eq!(Sut::sample(), Sut::sample()); + assert_eq!(Sut::sample_other(), Sut::sample_other()); + } + + #[test] + fn inequality() { + assert_ne!(Sut::sample(), Sut::sample_other()); + } + + #[test] + #[should_panic( + expected = "Invalid input, `owned_factor_instances` must not be empty." + )] + fn panics_if_owned_factors_is_empty() { + Sut::new( + IntentHash::sample(), + FactorSourceIDFromHash::sample(), + IndexSet::new(), + ); + } + + #[test] + #[should_panic( + expected = "Discrepancy! Mismatch between FactorSourceID of owned factor instances and specified FactorSourceID, this is a programmer error." + )] + fn panics_mismatch_factor_source_id() { + Sut::new( + IntentHash::sample_other(), + FactorSourceIDFromHash::sample_other(), + IndexSet::just(OwnedFactorInstance::sample_other()), + ); + } +} diff --git a/crates/sargon/src/signing/host_interaction/sign_interactors.rs b/crates/sargon/src/signing/host_interaction/sign_interactors.rs new file mode 100644 index 000000000..42c983a54 --- /dev/null +++ b/crates/sargon/src/signing/host_interaction/sign_interactors.rs @@ -0,0 +1,6 @@ +use crate::prelude::*; + +/// A collection of "interactors" which can sign transactions. +pub trait SignInteractors { + fn interactor_for(&self, kind: FactorSourceKind) -> SignInteractor; +} diff --git a/crates/sargon/src/signing/host_interaction/sign_response.rs b/crates/sargon/src/signing/host_interaction/sign_response.rs new file mode 100644 index 000000000..28cc1c101 --- /dev/null +++ b/crates/sargon/src/signing/host_interaction/sign_response.rs @@ -0,0 +1,31 @@ +use crate::prelude::*; + +/// The response of a batch signing request, either a PolyFactor or MonoFactor signing +/// request, matters not, because the goal is to have signed all transactions with +/// enough keys (derivation paths) needed for it to be valid when submitted to the +/// Radix network. +#[derive(Clone, PartialEq, Eq, derive_more::Debug)] +#[debug("SignResponse {{ signatures: {:#?} }}", signatures.values().map(|f| format!("{:#?}", f)).join(", "))] +pub struct SignResponse { + pub signatures: IndexMap>, +} + +impl SignResponse { + pub fn new( + signatures: IndexMap>, + ) -> Self { + Self { signatures } + } + + pub fn with_signatures(signatures: IndexSet) -> Self { + let signatures = signatures + .into_iter() + .into_group_map_by(|x| x.factor_source_id()); + Self::new( + signatures + .into_iter() + .map(|(k, v)| (k, IndexSet::from_iter(v))) + .collect(), + ) + } +} diff --git a/crates/sargon/src/signing/mod.rs b/crates/sargon/src/signing/mod.rs new file mode 100644 index 000000000..9716fba48 --- /dev/null +++ b/crates/sargon/src/signing/mod.rs @@ -0,0 +1,18 @@ +mod collector; +mod host_interaction; +mod petition_types; +mod signatures_outecome_types; +mod tx_to_sign; + +#[cfg(test)] +mod testing; + +pub(crate) use tx_to_sign::*; + +pub use collector::*; +pub use host_interaction::*; +pub use petition_types::*; +pub use signatures_outecome_types::*; + +#[cfg(test)] +pub(crate) use testing::*; diff --git a/crates/sargon/src/signing/petition_types/factor_list_kind.rs b/crates/sargon/src/signing/petition_types/factor_list_kind.rs new file mode 100644 index 000000000..acbc6e709 --- /dev/null +++ b/crates/sargon/src/signing/petition_types/factor_list_kind.rs @@ -0,0 +1,6 @@ +/// A kind of factor list, either threshold, or override kind. +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +pub(crate) enum FactorListKind { + Threshold, + Override, +} diff --git a/crates/sargon/src/signing/petition_types/general_role_with_hd_factor_instance.rs b/crates/sargon/src/signing/petition_types/general_role_with_hd_factor_instance.rs new file mode 100644 index 000000000..a11aae001 --- /dev/null +++ b/crates/sargon/src/signing/petition_types/general_role_with_hd_factor_instance.rs @@ -0,0 +1,196 @@ +use crate::prelude::*; + +decl_role_with_factors!( + /// A general depiction of each of the roles in a `MatrixOfFactorInstances`. + /// `SignaturesCollector` can work on any `RoleKind` when dealing with a securified entity. + General, + HierarchicalDeterministicFactorInstance +); + +impl TryFrom<(MatrixOfFactorInstances, RoleKind)> + for GeneralRoleWithHierarchicalDeterministicFactorInstances +{ + type Error = CommonError; + + fn try_from( + (matrix, role): (MatrixOfFactorInstances, RoleKind), + ) -> Result { + let (threshold_factors, threshold, override_factors) = match role { + RoleKind::Primary => ( + matrix.primary_role.threshold_factors, + matrix.primary_role.threshold, + matrix.primary_role.override_factors, + ), + RoleKind::Recovery => ( + matrix.recovery_role.threshold_factors, + matrix.recovery_role.threshold, + matrix.recovery_role.override_factors, + ), + RoleKind::Confirmation => ( + matrix.confirmation_role.threshold_factors, + matrix.confirmation_role.threshold, + matrix.confirmation_role.override_factors, + ), + }; + + GeneralRoleWithHierarchicalDeterministicFactorInstances::new( + threshold_factors + .iter() + .map(|f| HierarchicalDeterministicFactorInstance::try_from_factor_instance(f.clone())) + .collect::>>()?, + threshold, + override_factors + .iter() + .map(|f| HierarchicalDeterministicFactorInstance::try_from_factor_instance(f.clone())) + .collect::>>()?, + ) + } +} + +impl GeneralRoleWithHierarchicalDeterministicFactorInstances { + pub fn override_only( + factors: impl IntoIterator, + ) -> Self { + Self::new([], 0, factors) + .expect("Zero threshold with zero threshold factors and one override should not fail.") + } + + pub fn single_override( + factor: HierarchicalDeterministicFactorInstance, + ) -> Self { + Self::override_only([factor]) + } + + pub fn threshold_only( + factors: impl IntoIterator, + threshold: u8, + ) -> Result { + Self::new(factors, threshold, []) + } + + pub fn single_threshold( + factor: HierarchicalDeterministicFactorInstance, + ) -> Self { + Self::threshold_only([factor], 1).expect( + "Single threshold with one threshold factor should not fail.", + ) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = GeneralRoleWithHierarchicalDeterministicFactorInstances; + + #[test] + fn test_from_primary_role() { + assert_eq!( + GeneralRoleWithHierarchicalDeterministicFactorInstances::try_from( + (matrix(), RoleKind::Primary) + ).unwrap(), + SUT::new( + [HierarchicalDeterministicFactorInstance::try_from_factor_instance(FactorInstance::sample()).unwrap()], + 1, + [] + ).unwrap() + ) + } + + #[test] + fn test_from_recovery_role() { + assert_eq!( + GeneralRoleWithHierarchicalDeterministicFactorInstances::try_from( + (matrix(), RoleKind::Recovery) + ).unwrap(), + SUT::new( + [HierarchicalDeterministicFactorInstance::try_from_factor_instance( + FactorInstance::new( + FactorSourceIDFromHash::sample_ledger().into(), + FactorInstanceBadge::sample() + ) + ).unwrap()], + 1, + [] + ).unwrap() + ) + } + + #[test] + fn test_from_confirmation_role() { + assert_eq!( + GeneralRoleWithHierarchicalDeterministicFactorInstances::try_from( + (matrix(), RoleKind::Confirmation) + ).unwrap(), + SUT::new( + [HierarchicalDeterministicFactorInstance::try_from_factor_instance( + FactorInstance::new( + FactorSourceIDFromHash::sample_passphrase().into(), + FactorInstanceBadge::sample() + ) + ).unwrap()], + 1, + [] + ).unwrap() + ) + } + + #[test] + fn test_from_matrix_containing_physical_badge() { + let matrix = MatrixOfFactorInstances::new( + PrimaryRoleWithFactorInstances::new( + [FactorInstance::sample_other()], + 1, + [], + ) + .unwrap(), + recovery_role(), + confirmation_role(), + ); + + assert_eq!( + GeneralRoleWithHierarchicalDeterministicFactorInstances::try_from( + (matrix, RoleKind::Primary) + ), + Err(CommonError::BadgeIsNotVirtualHierarchicalDeterministic) + ); + } + + fn matrix() -> MatrixOfFactorInstances { + MatrixOfFactorInstances::new( + primary_role(), + recovery_role(), + confirmation_role(), + ) + } + + fn primary_role() -> PrimaryRoleWithFactorInstances { + PrimaryRoleWithFactorInstances::new([FactorInstance::sample()], 1, []) + .unwrap() + } + + fn recovery_role() -> RecoveryRoleWithFactorInstances { + RecoveryRoleWithFactorInstances::new( + [FactorInstance::new( + FactorSourceIDFromHash::sample_ledger().into(), + FactorInstanceBadge::sample(), + )], + 1, + [], + ) + .unwrap() + } + + fn confirmation_role() -> ConfirmationRoleWithFactorInstances { + ConfirmationRoleWithFactorInstances::new( + [FactorInstance::new( + FactorSourceIDFromHash::sample_passphrase().into(), + FactorInstanceBadge::sample(), + )], + 1, + [], + ) + .unwrap() + } +} diff --git a/crates/sargon/src/signing/petition_types/mod.rs b/crates/sargon/src/signing/petition_types/mod.rs new file mode 100644 index 000000000..a7f2827d3 --- /dev/null +++ b/crates/sargon/src/signing/petition_types/mod.rs @@ -0,0 +1,18 @@ +mod factor_list_kind; +mod general_role_with_hd_factor_instance; +mod petition_for_entity; +mod petition_for_factors_types; +mod petition_for_transaction; +mod petition_status; +mod petitions; +mod role_kind; + +pub(crate) use factor_list_kind::*; +pub(crate) use petition_for_entity::*; +pub(crate) use petition_for_transaction::*; +pub(crate) use petition_status::*; +pub(crate) use petitions::*; + +pub use general_role_with_hd_factor_instance::*; +pub use petition_for_factors_types::*; +pub use role_kind::*; diff --git a/crates/sargon/src/signing/petition_types/petition_for_entity.rs b/crates/sargon/src/signing/petition_types/petition_for_entity.rs new file mode 100644 index 000000000..960de46c0 --- /dev/null +++ b/crates/sargon/src/signing/petition_types/petition_for_entity.rs @@ -0,0 +1,704 @@ +use crate::prelude::*; + +/// Petition of signatures from an entity in a transaction. +/// Essentially a wrapper around a tuple +/// `{ threshold: PetitionForFactors, override: PetitionForFactors }` +#[derive(Clone, PartialEq, Eq, derive_more::Debug)] +#[debug("{}", self.debug_str())] +pub(crate) struct PetitionForEntity { + /// The owner of these factors + pub(crate) entity: AddressOfAccountOrPersona, + + /// Index and hash of transaction + pub(crate) intent_hash: IntentHash, + + /// Petition with threshold factors + pub(crate) threshold_factors: Option>, + + /// Petition with override factors + pub(crate) override_factors: Option>, +} + +impl PetitionForEntity { + pub(super) fn new( + intent_hash: IntentHash, + entity: AddressOfAccountOrPersona, + threshold_factors: impl Into>, + override_factors: impl Into>, + ) -> Self { + let threshold_factors = threshold_factors.into(); + let override_factors = override_factors.into(); + if threshold_factors.is_none() && override_factors.is_none() { + panic!("Programmer error! Must have at least one factors list."); + } + Self { + entity, + intent_hash, + threshold_factors: threshold_factors.map(RefCell::new), + override_factors: override_factors.map(RefCell::new), + } + } + + pub(crate) fn new_from_entity( + intent_hash: IntentHash, + entity: AccountOrPersona, + if_securified_select_role: RoleKind, + ) -> Self { + match entity.entity_security_state() { + EntitySecurityState::Unsecured { value } => { + let factor_instance = value.transaction_signing; + + Self::new_unsecurified( + intent_hash, + entity.address(), + factor_instance, + ) + } + EntitySecurityState::Securified { value } => { + let general_role = + GeneralRoleWithHierarchicalDeterministicFactorInstances::try_from( + (value.security_structure.matrix_of_factors, if_securified_select_role) + ).unwrap(); + + PetitionForEntity::new_securified( + intent_hash, + entity.address(), + general_role, + ) + } + } + } + + /// Creates a new Petition from an entity which is securified, i.e. has a matrix of factors. + pub(crate) fn new_securified( + intent_hash: IntentHash, + entity: AddressOfAccountOrPersona, + role_with_factor_instances: GeneralRoleWithHierarchicalDeterministicFactorInstances, + ) -> Self { + Self::new( + intent_hash, + entity, + PetitionForFactors::new_threshold( + role_with_factor_instances.threshold_factors, + role_with_factor_instances.threshold as i8, + ), + PetitionForFactors::new_override( + role_with_factor_instances.override_factors, + ), + ) + } + + /// Creates a new Petition from an entity which is unsecurified, i.e. has a single factor. + pub(crate) fn new_unsecurified( + intent_hash: IntentHash, + entity: AddressOfAccountOrPersona, + instance: HierarchicalDeterministicFactorInstance, + ) -> Self { + Self::new( + intent_hash, + entity, + PetitionForFactors::new_unsecurified(instance), + None, + ) + } + + /// Returns `true` if signatures requirement has been fulfilled, either by + /// override factors or by threshold factors + pub(crate) fn has_signatures_requirement_been_fulfilled(&self) -> bool { + self.status() + == PetitionForFactorsStatus::Finished( + PetitionFactorsStatusFinished::Success, + ) + } + + /// Returns `true` if the transaction of this petition already has failed due + /// to too many factors neglected + pub(crate) fn has_failed(&self) -> bool { + self.status() + == PetitionForFactorsStatus::Finished( + PetitionFactorsStatusFinished::Fail, + ) + } + + /// Returns the aggregate of **all** owned factor instances from both lists, either threshold or override. + pub(crate) fn all_factor_instances(&self) -> IndexSet { + self.access_both_list_then_form_union(|l| l.factor_instances()) + .into_iter() + .map(|f| { + OwnedFactorInstance::owned_factor_instance( + self.entity, + f.clone(), + ) + }) + .collect::>() + } + + /// Returns the aggregate of all **neglected** factor instances from both lists, either threshold or override, + /// that is, all factor instances but filtered out only those from FactorSources which have been neglected. + pub(crate) fn all_neglected_factor_instances( + &self, + ) -> IndexSet { + self.access_both_list_then_form_union(|f| f.all_neglected()) + } + + /// Returns the aggregate of all **neglected** factor sources from both lists, either threshold or override. + pub(crate) fn all_neglected_factor_sources( + &self, + ) -> IndexSet { + self.all_neglected_factor_instances() + .into_iter() + .map(|n| n.as_neglected_factor()) + .collect::>() + } + + /// Returrns the aggregate of all signatures from both lists, either threshold or override. + pub(crate) fn all_signatures(&self) -> IndexSet { + self.access_both_list_then_form_union(|f| f.all_signatures()) + } + + /// Mutates this petition by adding a signature to it. The signature is added to the relevant + /// list, either threshold or override. + /// + /// # Panics + /// Panics if this factor source has already been neglected or signed with. + /// + /// Or panics if the factor source is not known to this petition. + pub(crate) fn add_signature(&self, signature: HDSignature) { + self.access_both_list(|l| l.add_signature_if_relevant(&signature), |t, o| { + match (t, o) { + (Some(true), Some(true)) => { + unreachable!("Matrix of FactorInstances does not allow for a factor to be present in both threshold and override list, thus this will never happen.") + } + (Some(false), Some(false)) => panic!("Factor source not found in any of the lists."), + (None, None) => panic!("Programmer error! Must have at least one factors list."), + _ => (), + } + }) + } + + /// Queries if the authorization of the entity in this transaction already is irrelevant, since + /// too many factors have been neglected. + pub(crate) fn should_neglect_factors_due_to_irrelevant( + &self, + factor_source_ids: IndexSet, + ) -> bool { + assert!(self.references_any_factor_source(&factor_source_ids)); + match self.status() { + PetitionForFactorsStatus::Finished( + PetitionFactorsStatusFinished::Fail, + ) => true, + PetitionForFactorsStatus::Finished( + PetitionFactorsStatusFinished::Success, + ) => false, + PetitionForFactorsStatus::InProgress => false, + } + } + + /// Returns this petitions entity if the transaction would be invalid if the given factor sources + /// would be neglected. + pub(crate) fn invalid_transaction_if_neglected_factors( + &self, + factor_source_ids: IndexSet, + ) -> Option { + let status_if_neglected = + self.status_if_neglected_factors(factor_source_ids); + match status_if_neglected { + PetitionForFactorsStatus::Finished(finished_reason) => { + match finished_reason { + PetitionFactorsStatusFinished::Fail => Some(self.entity), + PetitionFactorsStatusFinished::Success => None, + } + } + PetitionForFactorsStatus::InProgress => None, + } + } + + pub(crate) fn status_if_neglected_factors( + &self, + factor_source_ids: IndexSet, + ) -> PetitionForFactorsStatus { + let simulation = self.clone(); + for factor_source_id in factor_source_ids.iter() { + simulation.neglect_if_referenced(NeglectedFactor::new( + NeglectFactorReason::Simulation, + *factor_source_id, + )) + } + simulation.status() + } + + /// Queries if this petition references any of the factor sources in the set of ids + /// by checking bot hteh threshold and the override factors list. + pub(crate) fn references_any_factor_source( + &self, + factor_source_ids: &IndexSet, + ) -> bool { + factor_source_ids + .iter() + .any(|f| self.references_factor_source_with_id(f)) + } + + /// Queries if this petition references the factor source with the given id, by + /// checking both the threshold and override factors list. + pub(crate) fn references_factor_source_with_id( + &self, + id: &FactorSourceIDFromHash, + ) -> bool { + self.access_both_list( + |p| p.references_factor_source_with_id(id), + |a, b| a.unwrap_or(false) || b.unwrap_or(false), + ) + } + + /// If this petitions references the neglected factor source, disregarding if it is a threshold + /// or override factor, it will be neglected. If the factor is not known to any of the lists + /// nothing happens. + pub(crate) fn neglect_if_referenced(&self, neglected: NeglectedFactor) { + self.access_both_list( + |p| p.neglect_if_referenced(neglected.clone()), + |_, _| (), + ); + } + + /// The "aggregated" status of this petition, i.e. the status of the threshold factors + /// and the status of the override factors merged together. E.g. (Threshold: InProgress, Override: InProgress) -> + /// Inprogress. And (Threshold: Finished(Fail), Override: InProgress) -> InProgress, + /// (Threshold: Finished(Fail), Override: Finished(Fail)) -> Finished(Fail) but + /// (Threshold: Finished(Success), Override: Inprogress) -> Finished(Success) - since + /// want to be able to finish early if the petition for this entity is already successful. + pub(crate) fn status(&self) -> PetitionForFactorsStatus { + use PetitionFactorsStatusFinished::*; + use PetitionForFactorsStatus::*; + + self.access_both_list( + |p| p.status(), + |maybe_threshold, maybe_override| { + if let Some(t) = &maybe_threshold { + trace!("Threshold factor status: {:?}", t); + } + if let Some(o) = &maybe_override { + trace!("Override factor status: {:?}", o); + } + match (maybe_threshold, maybe_override) { + (None, None) => { + panic!("Programmer error! Should have at least one factors list.") + } + (Some(threshold), None) => threshold, + (None, Some(r#override)) => r#override, + (Some(threshold), Some(r#override)) => match (threshold, r#override) { + (InProgress, InProgress) => PetitionForFactorsStatus::InProgress, + (Finished(Fail), InProgress) => PetitionForFactorsStatus::InProgress, + (InProgress, Finished(Fail)) => PetitionForFactorsStatus::InProgress, + (Finished(Fail), Finished(Fail)) => { + PetitionForFactorsStatus::Finished(Fail) + } + (Finished(Success), _) => PetitionForFactorsStatus::Finished(Success), + (_, Finished(Success)) => PetitionForFactorsStatus::Finished(Success), + }, + } + }, + ) + } +} + +// === Private === +impl PetitionForEntity { + /// Derefs and calls `access` on both lists respectively, if they exist. Then combines the results + /// of each list access using `combine`. + /// + /// This method is useful when you want to read out state for both list and somehow combine + /// that result, e.g. to form a union of all signatures - but not wanting to juggle `RefCell` + /// and `Option` repeatedly. + fn access_both_list( + &self, + access: impl Fn(&PetitionForFactors) -> T, + combine: impl Fn(Option, Option) -> U, + ) -> U { + let access_list_if_exists = + |list: &Option>| { + list.as_ref().map(|refcell| access(&refcell.borrow())) + }; + let t = access_list_if_exists(&self.threshold_factors); + let o = access_list_if_exists(&self.override_factors); + combine(t, o) + } + + /// Derefes and calls `access` on both lists respectively, if they exist. The result of the `access` + /// of each list is then combined together using `IndexSet::union` and returned. + fn access_both_list_then_form_union( + &self, + access: impl Fn(&PetitionForFactors) -> IndexSet, + ) -> IndexSet + where + T: Eq + std::hash::Hash + Clone, + { + self.access_both_list( + |l| access(l), + |t, o| { + t.unwrap_or_default() + .union(&o.unwrap_or_default()) + .cloned() + .collect::>() + }, + ) + } + + #[allow(unused)] + fn debug_str(&self) -> String { + let thres: String = self + .threshold_factors + .clone() + .map(|f| format!("threshold_factors {:#?}", f.borrow())) + .unwrap_or_default(); + + let overr: String = self + .override_factors + .clone() + .map(|f| format!("override_factors {:#?}", f.borrow())) + .unwrap_or_default(); + + format!( + "intent_hash: {:#?}, entity: {:#?}, {:#?}{:#?}", + self.intent_hash, self.entity, thres, overr + ) + } +} + +// === SAMPLE VALUES === +impl PetitionForEntity { + fn from_entity_with_role_kind( + entity: impl Into, + intent_hash: IntentHash, + role_kind: RoleKind, + ) -> Self { + let entity = entity.into(); + match entity.entity_security_state() { + EntitySecurityState::Unsecured { value } => { + Self::new_unsecurified(intent_hash, entity.address(), value.transaction_signing) + } + EntitySecurityState::Securified { value } => { + Self::new_securified( + intent_hash, + entity.address(), + GeneralRoleWithHierarchicalDeterministicFactorInstances::try_from( + (value.security_structure.matrix_of_factors, role_kind) + ).unwrap() + ) + } + } + } +} + +impl HasSampleValues for PetitionForEntity { + fn sample() -> Self { + Self::from_entity_with_role_kind( + Account::sample_securified_mainnet( + "Grace", + AccountAddress::sample_other(), + || { + GeneralRoleWithHierarchicalDeterministicFactorInstances::r6(HierarchicalDeterministicFactorInstance::sample_id_to_instance( + CAP26EntityKind::Account, + HDPathComponent::from(6) + )) + }, + ), + IntentHash::sample(), + RoleKind::Primary, + ) + } + + fn sample_other() -> Self { + Self::from_entity_with_role_kind( + Account::sample_unsecurified_mainnet( + "Sample Unsec", + HierarchicalDeterministicFactorInstance::sample_fi0( + CAP26EntityKind::Account, + ), + ), + IntentHash::sample_other(), + RoleKind::Primary, + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + type Sut = PetitionForEntity; + + #[test] + fn multiple_device_as_override_skipped_both_is_invalid() { + let d0 = HierarchicalDeterministicFactorInstance::sample_fi0( + CAP26EntityKind::Account, + ); + let d1 = HierarchicalDeterministicFactorInstance::sample_fi10( + CAP26EntityKind::Account, + ); + assert_eq!(d0.factor_source_id.kind, FactorSourceKind::Device); + assert_eq!(d1.factor_source_id.kind, FactorSourceKind::Device); + + let matrix = + GeneralRoleWithHierarchicalDeterministicFactorInstances::override_only([d0.clone(), d1.clone()]); + let entity = AddressOfAccountOrPersona::from(AccountAddress::sample()); + let tx = IntentHash::new(Hash::sample_third(), NetworkID::Mainnet); + let sut = Sut::new_securified(tx.clone(), entity, matrix); + let invalid = + sut.invalid_transaction_if_neglected_factors(IndexSet::from_iter( + [d0.factor_source_id(), d1.factor_source_id()], + )) + .unwrap(); + + assert_eq!(invalid.clone(), entity); + } + + #[test] + fn multiple_device_as_override_skipped_one_is_valid() { + let d0 = HierarchicalDeterministicFactorInstance::sample_fi0( + CAP26EntityKind::Account, + ); + let d1 = HierarchicalDeterministicFactorInstance::sample_fi10( + CAP26EntityKind::Account, + ); + assert_eq!(d0.factor_source_id.kind, FactorSourceKind::Device); + assert_eq!(d1.factor_source_id.kind, FactorSourceKind::Device); + + let matrix = + GeneralRoleWithHierarchicalDeterministicFactorInstances::override_only( + [d0.clone(), d1.clone()] + ); + let entity = AddressOfAccountOrPersona::from(AccountAddress::sample()); + let tx = IntentHash::new(Hash::sample_third(), NetworkID::Mainnet); + let sut = Sut::new_securified(tx.clone(), entity, matrix); + let invalid = sut.invalid_transaction_if_neglected_factors( + IndexSet::just(d0.factor_source_id()), + ); + assert!(invalid.is_none()); + } + + #[test] + fn multiple_device_as_threshold_skipped_both_is_invalid() { + let d0 = HierarchicalDeterministicFactorInstance::sample_fi0( + CAP26EntityKind::Account, + ); + let d1 = HierarchicalDeterministicFactorInstance::sample_fi10( + CAP26EntityKind::Account, + ); + assert_eq!(d0.factor_source_id.kind, FactorSourceKind::Device); + assert_eq!(d1.factor_source_id.kind, FactorSourceKind::Device); + + let matrix = GeneralRoleWithHierarchicalDeterministicFactorInstances::threshold_only( + [d0.clone(), d1.clone()], + 2, + ).unwrap(); + + let entity = AddressOfAccountOrPersona::from(AccountAddress::sample()); + let tx = IntentHash::new(Hash::sample_third(), NetworkID::Mainnet); + let sut = Sut::new_securified(tx.clone(), entity, matrix); + let invalid = + sut.invalid_transaction_if_neglected_factors(IndexSet::from_iter( + [d0.factor_source_id(), d1.factor_source_id()], + )) + .unwrap(); + assert_eq!(invalid, entity); + } + + #[test] + fn two_device_as_threshold_of_2_skipped_one_is_invalid() { + let d0 = HierarchicalDeterministicFactorInstance::sample_fi0( + CAP26EntityKind::Account, + ); + let d1 = HierarchicalDeterministicFactorInstance::sample_fi10( + CAP26EntityKind::Account, + ); + assert_eq!(d0.factor_source_id.kind, FactorSourceKind::Device); + assert_eq!(d1.factor_source_id.kind, FactorSourceKind::Device); + + let matrix = GeneralRoleWithHierarchicalDeterministicFactorInstances::threshold_only( + [d0.clone(), d1.clone()], + 2, + ).unwrap(); + + let entity = AddressOfAccountOrPersona::from(AccountAddress::sample()); + let tx = IntentHash::new(Hash::sample_third(), NetworkID::Mainnet); + let sut = Sut::new_securified(tx.clone(), entity, matrix); + + let invalid = sut + .invalid_transaction_if_neglected_factors(IndexSet::just( + d1.factor_source_id(), + )) + .unwrap(); + + assert_eq!(invalid, entity); + } + + #[test] + fn two_device_as_threshold_of_1_skipped_one_is_valid() { + let d0 = HierarchicalDeterministicFactorInstance::sample_fi0( + CAP26EntityKind::Account, + ); + let d1 = HierarchicalDeterministicFactorInstance::sample_fi10( + CAP26EntityKind::Account, + ); + assert_eq!(d0.factor_source_id.kind, FactorSourceKind::Device); + assert_eq!(d1.factor_source_id.kind, FactorSourceKind::Device); + + let matrix = GeneralRoleWithHierarchicalDeterministicFactorInstances::threshold_only( + [d0.clone(), d1.clone()], + 1, + ).unwrap(); + + let entity = AddressOfAccountOrPersona::from(AccountAddress::sample()); + let tx = IntentHash::new(Hash::sample_third(), NetworkID::Mainnet); + let sut = Sut::new_securified(tx.clone(), entity, matrix); + + let invalid = sut.invalid_transaction_if_neglected_factors( + IndexSet::just(d1.factor_source_id()), + ); + + assert!(invalid.is_none()); + } + + #[test] + fn debug() { + assert!(!format!("{:?}", Sut::sample()).is_empty()); + } + + #[test] + #[should_panic( + expected = "Programmer error! Must have at least one factors list." + )] + fn invalid_empty_factors() { + Sut::new( + IntentHash::sample(), + AddressOfAccountOrPersona::sample(), + None, + None, + ); + } + + #[test] + #[should_panic(expected = "Factor source not found in any of the lists.")] + fn cannot_add_unrelated_signature() { + let sut = Sut::sample(); + sut.add_signature(HDSignature::sample()); + } + + #[test] + fn factor_should_not_be_used_in_both_lists() { + let fi = HierarchicalDeterministicFactorInstance::sample_id_to_instance( + CAP26EntityKind::Account, + HDPathComponent::from(0), + ); + assert_eq!( + GeneralRoleWithHierarchicalDeterministicFactorInstances::new( + [FactorSourceIDFromHash::sample_at(0)].map(&fi), + 1, + [FactorSourceIDFromHash::sample_at(0)].map(&fi), + ), + Err(CommonError::InvalidSecurityStructureFactorInBothThresholdAndOverride) + ); + } + + #[test] + fn threshold_should_not_be_bigger_than_threshold_factors() { + let fi = HierarchicalDeterministicFactorInstance::sample_id_to_instance( + CAP26EntityKind::Account, + HDPathComponent::from(0), + ); + assert_eq!( + GeneralRoleWithHierarchicalDeterministicFactorInstances::new( + [FactorSourceIDFromHash::sample_at(0)].map(&fi), + 2, + [], + ), + Err( + CommonError::InvalidSecurityStructureThresholdExceedsFactors { + threshold: 2, + factors: 1, + } + ) + ); + } + + #[test] + #[should_panic] + fn cannot_add_same_signature_twice() { + let intent_hash = IntentHash::sample(); + let entity = Account::sample_securified_mainnet( + "Alice", + AccountAddress::sample(), + || { + let fi = HierarchicalDeterministicFactorInstance::sample_id_to_instance( + CAP26EntityKind::Account, + HDPathComponent::from(0) + ); + GeneralRoleWithHierarchicalDeterministicFactorInstances::new( + [FactorSourceIDFromHash::sample_at(0)].map(&fi), + 1, + [FactorSourceIDFromHash::sample_at(1)].map(&fi), + ) + .unwrap() + }, + ); + let sut = Sut::from_entity_with_role_kind( + entity.clone(), + intent_hash.clone(), + RoleKind::Primary, + ); + + let sign_input = HDSignatureInput::new( + intent_hash, + OwnedFactorInstance::new( + AddressOfAccountOrPersona::from(entity.address), + HierarchicalDeterministicFactorInstance::sample_mainnet_tx_account( + HDPathComponent::from(0), + FactorSourceIDFromHash::sample_at(0), + ), + ), + ); + let signature = HDSignature::produced_signing_with_input(sign_input); + + sut.add_signature(signature.clone()); + sut.add_signature(signature.clone()); + } + + #[test] + fn invalid_transactions_if_neglected_success() { + let sut = Sut::sample(); + let signature = HDSignature::produced_signing_with_input( + HDSignatureInput::new( + sut.intent_hash.clone(), + OwnedFactorInstance::new( + sut.entity, + HierarchicalDeterministicFactorInstance::sample_mainnet_tx_account( + HDPathComponent::from(6), + FactorSourceIDFromHash::sample_at(1), + ), + ), + ) + ); + sut.add_signature(signature); + let can_skip = |f: FactorSourceIDFromHash| { + assert!(sut + // Already signed with override factor `FactorSourceIDFromHash::fs1()`. Thus + // can skip + .invalid_transaction_if_neglected_factors(IndexSet::just(f)) + .is_none()) + }; + can_skip(FactorSourceIDFromHash::sample_at(0)); + can_skip(FactorSourceIDFromHash::sample_at(3)); + can_skip(FactorSourceIDFromHash::sample_at(4)); + can_skip(FactorSourceIDFromHash::sample_at(5)); + } + + #[test] + fn inequality() { + assert_ne!(Sut::sample(), Sut::sample_other()) + } + + #[test] + fn equality() { + assert_eq!(Sut::sample(), Sut::sample()); + assert_eq!(Sut::sample_other(), Sut::sample_other()); + } +} diff --git a/crates/sargon/src/signing/petition_types/petition_for_factors_types/factor_source_referencing.rs b/crates/sargon/src/signing/petition_types/petition_for_factors_types/factor_source_referencing.rs new file mode 100644 index 000000000..3f0a90688 --- /dev/null +++ b/crates/sargon/src/signing/petition_types/petition_for_factors_types/factor_source_referencing.rs @@ -0,0 +1,22 @@ +use crate::prelude::*; + +/// A trait for types which reference a factor source. +pub(crate) trait FactorSourceReferencing: + std::hash::Hash + PartialEq + Eq + Clone +{ + fn factor_source_id(&self) -> FactorSourceIDFromHash; +} + +impl FactorSourceReferencing for HierarchicalDeterministicFactorInstance { + fn factor_source_id(&self) -> FactorSourceIDFromHash { + self.factor_source_id + } +} + +impl FactorSourceReferencing for HDSignature { + fn factor_source_id(&self) -> FactorSourceIDFromHash { + self.owned_factor_instance() + .factor_instance() + .factor_source_id + } +} diff --git a/crates/sargon/src/signing/petition_types/petition_for_factors_types/mod.rs b/crates/sargon/src/signing/petition_types/petition_for_factors_types/mod.rs new file mode 100644 index 000000000..fabdddcf0 --- /dev/null +++ b/crates/sargon/src/signing/petition_types/petition_for_factors_types/mod.rs @@ -0,0 +1,8 @@ +mod factor_source_referencing; +mod neglected_factor_instance; +mod petition_for_factors; + +pub(crate) use factor_source_referencing::*; +pub(crate) use petition_for_factors::*; + +pub use neglected_factor_instance::*; diff --git a/crates/sargon/src/signing/petition_types/petition_for_factors_types/neglected_factor_instance.rs b/crates/sargon/src/signing/petition_types/petition_for_factors_types/neglected_factor_instance.rs new file mode 100644 index 000000000..90759941f --- /dev/null +++ b/crates/sargon/src/signing/petition_types/petition_for_factors_types/neglected_factor_instance.rs @@ -0,0 +1,102 @@ +use crate::prelude::*; + +/// A neglected factor, with a reason. +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct AbstractNeglectedFactor { + /// The reason why this factor was neglected. + pub(crate) reason: NeglectFactorReason, + + /// The neglected factor + pub(crate) content: T, +} + +impl AbstractNeglectedFactor { + pub fn new(reason: NeglectFactorReason, content: T) -> Self { + Self { reason, content } + } +} + +impl Debug for AbstractNeglectedFactor { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Neglected") + .field("reason", &self.reason) + .field("content", &self.content) + .finish() + } +} + +impl NeglectedFactorInstance { + /// Maps from `Neglected` + /// to `Neglected`, + pub(crate) fn as_neglected_factor(&self) -> NeglectedFactor { + NeglectedFactor::new(self.reason, self.factor_source_id()) + } +} +impl FactorSourceReferencing for NeglectedFactorInstance { + fn factor_source_id(&self) -> FactorSourceIDFromHash { + self.content.factor_source_id() + } +} + +impl FactorSourceReferencing for NeglectedFactor { + fn factor_source_id(&self) -> FactorSourceIDFromHash { + self.content + } +} + +impl HasSampleValues for NeglectedFactorInstance { + fn sample() -> Self { + Self::new( + NeglectFactorReason::UserExplicitlySkipped, + HierarchicalDeterministicFactorInstance::sample(), + ) + } + fn sample_other() -> Self { + Self::new( + NeglectFactorReason::Failure, + HierarchicalDeterministicFactorInstance::sample_other(), + ) + } +} + +/// ID to some neglected factor source, with the reason why it was neglected (skipped/failed) +pub(crate) type NeglectedFactor = + AbstractNeglectedFactor; + +/// IDs to some neglected factor source, with the reason why they were neglected (skipped/failed) +pub type NeglectedFactors = + AbstractNeglectedFactor>; + +/// A HierarchicalDeterministicFactorInstance which was rejected, with the reason why (skipped/failed) +pub(crate) type NeglectedFactorInstance = + AbstractNeglectedFactor; + +/// Reason why some FactorSource was neglected, either explicitly skipped by the user +/// or implicitly neglected due to failure. +#[derive( + Clone, Copy, PartialEq, Eq, Hash, derive_more::Debug, derive_more::Display, +)] +pub enum NeglectFactorReason { + /// A FactorSource got neglected since user explicitly skipped it. + #[display("User Skipped")] + #[debug("UserExplicitlySkipped")] + UserExplicitlySkipped, + + /// A FactorSource got neglected implicitly due to failure + #[display("Failure")] + #[debug("Failure")] + Failure, + + /// A FactorSource got neglected implicitly since it is irrelevant, + /// all transactions which references the FactorSource have already + /// failed, thus pointless in using it. + #[display("Irrelevant")] + #[debug("Irrelevant")] + Irrelevant, + + /// We simulate neglect in order to see what the status of petitions + /// would be if a FactorSource would be neglected. + #[display("Simulation")] + #[debug("Simulation")] + Simulation, +} diff --git a/crates/sargon/src/signing/petition_types/petition_for_factors_types/petition_for_factors/mod.rs b/crates/sargon/src/signing/petition_types/petition_for_factors_types/petition_for_factors/mod.rs new file mode 100644 index 000000000..937d33595 --- /dev/null +++ b/crates/sargon/src/signing/petition_types/petition_for_factors_types/petition_for_factors/mod.rs @@ -0,0 +1,16 @@ +#[allow(clippy::module_inception)] +mod petition_for_factors; + +mod petition_for_factors_input; +mod petition_for_factors_state; +mod petition_for_factors_state_snapshot; +mod petition_for_factors_status; +mod petition_for_factors_sub_state; + +use petition_for_factors_input::*; +use petition_for_factors_state::*; +use petition_for_factors_state_snapshot::*; +use petition_for_factors_sub_state::*; + +pub(crate) use petition_for_factors::*; +pub(crate) use petition_for_factors_status::*; diff --git a/crates/sargon/src/signing/petition_types/petition_for_factors_types/petition_for_factors/petition_for_factors.rs b/crates/sargon/src/signing/petition_types/petition_for_factors_types/petition_for_factors/petition_for_factors.rs new file mode 100644 index 000000000..58a0b4606 --- /dev/null +++ b/crates/sargon/src/signing/petition_types/petition_for_factors_types/petition_for_factors/petition_for_factors.rs @@ -0,0 +1,220 @@ +use super::*; +use crate::prelude::*; + +/// Petition of signatures from a factors list of an entity in a transaction. +#[derive(Clone, PartialEq, Eq, derive_more::Debug)] +#[debug("{}", self.debug_str())] +pub(crate) struct PetitionForFactors { + pub(crate) factor_list_kind: FactorListKind, + + /// Factors to sign with and the required number of them. + pub(crate) input: PetitionForFactorsInput, + state: RefCell, +} + +impl HasSampleValues for PetitionForFactors { + fn sample() -> Self { + Self::new(FactorListKind::Threshold, PetitionForFactorsInput::sample()) + } + + fn sample_other() -> Self { + Self::new( + FactorListKind::Override, + PetitionForFactorsInput::sample_other(), + ) + } +} + +impl PetitionForFactors { + pub(crate) fn new( + factor_list_kind: FactorListKind, + input: PetitionForFactorsInput, + ) -> Self { + Self { + factor_list_kind, + input, + state: RefCell::new(PetitionForFactorsState::new()), + } + } + + pub(crate) fn factor_instances( + &self, + ) -> IndexSet { + self.input.factors.clone() + } + + pub(crate) fn all_neglected(&self) -> IndexSet { + self.state.borrow().all_neglected() + } + + pub(crate) fn all_signatures(&self) -> IndexSet { + self.state.borrow().all_signatures() + } + + pub(crate) fn new_threshold( + factors: Vec, + threshold: i8, + ) -> Option { + if factors.is_empty() { + return None; + } + Some(Self::new( + FactorListKind::Threshold, + PetitionForFactorsInput::new_threshold( + IndexSet::from_iter(factors), + threshold, + ), + )) + } + + pub(crate) fn new_unsecurified( + factor: HierarchicalDeterministicFactorInstance, + ) -> Self { + Self::new_threshold(vec![factor], 1).expect("Factors is not empty") // define as 1/1 threshold factor, which is a good definition. + } + + pub(crate) fn new_override( + factors: Vec, + ) -> Option { + if factors.is_empty() { + return None; + } + Some(Self::new( + FactorListKind::Override, + PetitionForFactorsInput::new_override(IndexSet::from_iter(factors)), + )) + } + + pub(crate) fn neglect_if_referenced(&self, neglected: NeglectedFactor) { + let factor_source_id = &neglected.factor_source_id(); + if let Some(_x_) = + self.reference_to_factor_source_with_id(factor_source_id) + { + debug!( + "PetitionForFactors = kind {:?} neglect factor source with id: {}, reason: {}", + self.factor_list_kind, factor_source_id, neglected.reason + ); + self.neglect(neglected) + } else { + debug!( + "PetitionForFactors = kind {:?} did not reference factor source with id: {}", + self.factor_list_kind, factor_source_id + ); + } + } + + fn neglect(&self, neglected: NeglectedFactor) { + let factor_instance = self.expect_reference_to_factor_source_with_id( + &neglected.factor_source_id(), + ); + self.state + .borrow_mut() + .neglect(&NeglectedFactorInstance::new( + neglected.reason, + factor_instance.clone(), + )); + } + + pub(crate) fn has_owned_instance_with_id( + &self, + owned_factor_instance: &OwnedFactorInstance, + ) -> bool { + self.has_instance_with_id(owned_factor_instance.factor_instance()) + } + + pub(crate) fn has_instance_with_id( + &self, + factor_instance: &HierarchicalDeterministicFactorInstance, + ) -> bool { + self.input.factors.iter().any(|f| f == factor_instance) + } + + pub(crate) fn add_signature_if_relevant( + &self, + signature: &HDSignature, + ) -> bool { + if self.has_owned_instance_with_id(signature.owned_factor_instance()) { + self.add_signature(signature); + true + } else { + false + } + } + + /// # Panics + /// Panics if this factor source has already been neglected or signed with. + fn add_signature(&self, signature: &HDSignature) { + let state = self.state.borrow_mut(); + state.add_signature(signature) + } + + pub(crate) fn references_factor_source_with_id( + &self, + factor_source_id: &FactorSourceIDFromHash, + ) -> bool { + self.reference_to_factor_source_with_id(factor_source_id) + .is_some() + } + + fn expect_reference_to_factor_source_with_id( + &self, + factor_source_id: &FactorSourceIDFromHash, + ) -> &HierarchicalDeterministicFactorInstance { + self.reference_to_factor_source_with_id(factor_source_id) + .expect("Programmer error! Factor source not found in factors.") + } + + fn reference_to_factor_source_with_id( + &self, + factor_source_id: &FactorSourceIDFromHash, + ) -> Option<&HierarchicalDeterministicFactorInstance> { + self.input.reference_factor_source_with_id(factor_source_id) + } + + fn state_snapshot(&self) -> PetitionForFactorsStateSnapshot { + self.state.borrow().snapshot() + } + + fn is_finished_successfully(&self) -> bool { + self.input.is_fulfilled_by(self.state_snapshot()) + } + + fn is_finished_with_fail(&self) -> bool { + let snapshot = self.state_snapshot(); + let is_finished_with_fail = + self.input.is_failure_with(snapshot.clone()); + trace!( + "is_finished_with_fail: {:?} from input: {:?}, snapshot: {:?}", + is_finished_with_fail, + self.input, + snapshot + ); + is_finished_with_fail + } + + fn get_finished_with(&self) -> Option { + if self.is_finished_successfully() { + Some(PetitionFactorsStatusFinished::Success) + } else if self.is_finished_with_fail() { + Some(PetitionFactorsStatusFinished::Fail) + } else { + None + } + } + + pub(crate) fn status(&self) -> PetitionForFactorsStatus { + if let Some(finished_state) = self.get_finished_with() { + return PetitionForFactorsStatus::Finished(finished_state); + } + PetitionForFactorsStatus::InProgress + } + + #[allow(unused)] + pub(crate) fn debug_str(&self) -> String { + format!( + "PetitionForFactors(input: {:#?}, state_snapshot: {:#?})", + self.input, + self.state_snapshot() + ) + } +} diff --git a/crates/sargon/src/signing/petition_types/petition_for_factors_types/petition_for_factors/petition_for_factors_input.rs b/crates/sargon/src/signing/petition_types/petition_for_factors_types/petition_for_factors/petition_for_factors_input.rs new file mode 100644 index 000000000..eec3787b7 --- /dev/null +++ b/crates/sargon/src/signing/petition_types/petition_for_factors_types/petition_for_factors/petition_for_factors_input.rs @@ -0,0 +1,106 @@ +use super::*; +use crate::prelude::*; + +/// The input passed to a PetitionsForFactors +#[derive(Clone, PartialEq, Eq, derive_more::Debug)] +#[debug("PetitionForFactorsInput(factors: {:#?})", self.factors)] +pub(crate) struct PetitionForFactorsInput { + /// Factors to sign with. + pub(super) factors: IndexSet, + + /// Number of required factors to sign with. + pub(super) required: i8, +} + +impl HasSampleValues for PetitionForFactorsInput { + fn sample() -> Self { + Self::new( + IndexSet::from_iter([ + HierarchicalDeterministicFactorInstance::sample(), + HierarchicalDeterministicFactorInstance::sample_other(), + ]), + 1, + ) + } + + fn sample_other() -> Self { + Self::new( + IndexSet::from_iter([ + HierarchicalDeterministicFactorInstance::sample_other(), + ]), + 1, + ) + } +} + +impl PetitionForFactorsInput { + pub(super) fn new( + factors: IndexSet, + required: i8, + ) -> Self { + Self { factors, required } + } + + pub(super) fn new_threshold( + factors: IndexSet, + threshold: i8, + ) -> Self { + Self::new(factors, threshold) + } + + pub(super) fn new_override( + factors: IndexSet, + ) -> Self { + Self::new(factors, 1) // we need just one, anyone, factor for threshold. + } + + pub(crate) fn reference_factor_source_with_id( + &self, + factor_source_id: &FactorSourceIDFromHash, + ) -> Option<&HierarchicalDeterministicFactorInstance> { + self.factors + .iter() + .find(|f| f.factor_source_id == *factor_source_id) + } + + fn factors_count(&self) -> i8 { + self.factors.len() as i8 + } + + fn remaining_factors_until_success( + &self, + snapshot: PetitionForFactorsStateSnapshot, + ) -> i8 { + self.required - snapshot.signed_count() + } + + pub(super) fn is_fulfilled_by( + &self, + snapshot: PetitionForFactorsStateSnapshot, + ) -> bool { + self.remaining_factors_until_success(snapshot) <= 0 + } + + fn factors_left_to_prompt( + &self, + snapshot: PetitionForFactorsStateSnapshot, + ) -> i8 { + self.factors_count() - snapshot.prompted_count() + } + + pub(super) fn is_failure_with( + &self, + snapshot: PetitionForFactorsStateSnapshot, + ) -> bool { + let signed_or_pending = self.factors_left_to_prompt(snapshot.clone()) + + snapshot.signed_count(); + let is_failure_with = signed_or_pending < self.required; + trace!( + "is_failure_with: {:?}, signed_or_pending: {:?}, required: {:?}", + is_failure_with, + signed_or_pending, + self.required + ); + is_failure_with + } +} diff --git a/crates/sargon/src/signing/petition_types/petition_for_factors_types/petition_for_factors/petition_for_factors_state.rs b/crates/sargon/src/signing/petition_types/petition_for_factors_types/petition_for_factors/petition_for_factors_state.rs new file mode 100644 index 000000000..0069e4ed5 --- /dev/null +++ b/crates/sargon/src/signing/petition_types/petition_for_factors_types/petition_for_factors/petition_for_factors_state.rs @@ -0,0 +1,197 @@ +use std::cell::Ref; + +use super::*; +use crate::prelude::*; + +/// Mutable state of `PetitionForFactors`, keeping track of which factors that +/// have either signed or been neglected. +#[derive(Clone, PartialEq, Eq, derive_more::Debug)] +#[debug("PetitionForFactorsState(signed: {:?}, neglected: {:?})", signed.borrow().clone(), neglected.borrow().clone())] +pub(crate) struct PetitionForFactorsState { + /// Factors that have signed. + signed: RefCell>, + + /// Neglected factors, either due to user explicitly skipping, or due + /// implicitly neglected to failure. + neglected: RefCell>, +} + +impl PetitionForFactorsState { + /// Creates a new `PetitionForFactorsState`. + pub(super) fn new() -> Self { + Self { + signed: RefCell::new(PetitionForFactorsSubState::<_>::new()), + neglected: RefCell::new(PetitionForFactorsSubState::<_>::new()), + } + } + + /// A reference to the neglected factors so far. + pub(super) fn neglected( + &self, + ) -> Ref> { + self.neglected.borrow() + } + + /// A reference to the factors which have been signed with so far. + pub(super) fn signed( + &self, + ) -> Ref> { + self.signed.borrow() + } + + /// A set of signatures from factors that have been signed with so far. + pub(crate) fn all_signatures(&self) -> IndexSet { + self.signed().snapshot() + } + + /// A set factors have been neglected so far. + pub(crate) fn all_neglected(&self) -> IndexSet { + self.neglected().snapshot() + } + + /// # Panics + /// Panics if this factor source has already been neglected or signed with. + fn assert_not_referencing_factor_source( + &self, + factor_source_id: FactorSourceIDFromHash, + ) { + assert!( + !self.references_factor_source_by_id(factor_source_id), + "Programmer error! Factor source {:#?} already used, should only be referenced once.", + factor_source_id, + ); + } + + /// # Panics + /// Panics if this factor source has already been neglected or signed and + /// this is not a simulation. + pub(crate) fn neglect(&self, neglected: &NeglectedFactorInstance) { + if neglected.reason != NeglectFactorReason::Simulation { + self.assert_not_referencing_factor_source( + neglected.factor_source_id(), + ); + } + self.neglected.borrow_mut().insert(neglected); + } + + /// # Panics + /// Panics if this factor source has already been neglected or signed with. + pub(crate) fn add_signature(&self, signature: &HDSignature) { + self.assert_not_referencing_factor_source(signature.factor_source_id()); + self.signed.borrow_mut().insert(signature) + } + + pub(super) fn snapshot(&self) -> PetitionForFactorsStateSnapshot { + PetitionForFactorsStateSnapshot::new( + self.signed().snapshot(), + self.neglected().snapshot(), + ) + } + + fn references_factor_source_by_id( + &self, + factor_source_id: FactorSourceIDFromHash, + ) -> bool { + self.signed() + .references_factor_source_by_id(factor_source_id) + || self + .neglected() + .references_factor_source_by_id(factor_source_id) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + type Sut = PetitionForFactorsState; + + impl PetitionForFactorsState { + fn test_neglect( + &self, + id: &HierarchicalDeterministicFactorInstance, + simulated: bool, + ) { + self.neglect(&NeglectedFactorInstance::new( + if simulated { + NeglectFactorReason::Simulation + } else { + NeglectFactorReason::UserExplicitlySkipped + }, + id.clone(), + )) + } + } + + #[test] + #[should_panic] + fn skipping_twice_panics() { + let sut = Sut::new(); + let fi = HierarchicalDeterministicFactorInstance::sample(); + sut.test_neglect(&fi, false); + sut.test_neglect(&fi, false); + } + + #[test] + #[should_panic] + fn signing_twice_panics() { + let sut = Sut::new(); + let sig = HDSignature::sample(); + sut.add_signature(&sig); + sut.add_signature(&sig); + } + + #[test] + #[should_panic] + fn skipping_already_signed_panics() { + let sut = Sut::new(); + + let intent_hash = IntentHash::sample(); + + let factor_instance = + HierarchicalDeterministicFactorInstance::sample_mainnet_tx_account( + HDPathComponent::from(0), + FactorSourceIDFromHash::sample_at(0), + ); + let sign_input = HDSignatureInput::new( + intent_hash, + OwnedFactorInstance::new( + AddressOfAccountOrPersona::sample(), + factor_instance.clone(), + ), + ); + let signature = HDSignature::produced_signing_with_input(sign_input); + + sut.add_signature(&signature); + + sut.test_neglect(&factor_instance, false); + } + + #[test] + #[should_panic] + fn signing_already_skipped_panics() { + let sut = Sut::new(); + + let intent_hash = IntentHash::sample(); + let factor_instance = + HierarchicalDeterministicFactorInstance::sample_mainnet_tx_account( + HDPathComponent::from(0), + FactorSourceIDFromHash::sample_at(0), + ); + + sut.test_neglect(&factor_instance, false); + + let sign_input = HDSignatureInput::new( + intent_hash, + OwnedFactorInstance::new( + AddressOfAccountOrPersona::sample(), + factor_instance.clone(), + ), + ); + + let signature = HDSignature::produced_signing_with_input(sign_input); + + sut.add_signature(&signature); + } +} diff --git a/crates/sargon/src/signing/petition_types/petition_for_factors_types/petition_for_factors/petition_for_factors_state_snapshot.rs b/crates/sargon/src/signing/petition_types/petition_for_factors_types/petition_for_factors/petition_for_factors_state_snapshot.rs new file mode 100644 index 000000000..42bbdb6ba --- /dev/null +++ b/crates/sargon/src/signing/petition_types/petition_for_factors_types/petition_for_factors/petition_for_factors_state_snapshot.rs @@ -0,0 +1,96 @@ +use crate::prelude::*; + +/// An immutable "snapshot" of `PetitionForFactorsState` +#[derive(Clone, PartialEq, Eq, derive_more::Debug)] +#[debug("{}", self.debug_str())] +pub(super) struct PetitionForFactorsStateSnapshot { + /// Factors that have signed. + signed: IndexSet, + + /// Factors that has been neglected. + neglected: IndexSet, +} + +impl PetitionForFactorsStateSnapshot { + pub(super) fn new( + signed: IndexSet, + neglected: IndexSet, + ) -> Self { + Self { signed, neglected } + } + + pub(super) fn prompted_count(&self) -> i8 { + self.signed_count() + self.neglected_count() + } + + pub(super) fn signed_count(&self) -> i8 { + self.signed.len() as i8 + } + + fn neglected_count(&self) -> i8 { + self.neglected.len() as i8 + } + + #[allow(unused)] + fn debug_str(&self) -> String { + let signatures = self + .signed + .clone() + .into_iter() + .map(|s| format!("{:?}", s)) + .join(", "); + + let neglected = self + .neglected + .clone() + .into_iter() + .map(|s| format!("{:?}", s)) + .join(", "); + + format!("signatures: {:#?}, neglected: {:#?}", signatures, neglected) + } +} + +impl HasSampleValues for PetitionForFactorsStateSnapshot { + fn sample() -> Self { + Self::new( + IndexSet::from_iter([ + HDSignature::sample(), + HDSignature::sample_other(), + ]), + IndexSet::from_iter([ + NeglectedFactorInstance::sample(), + NeglectedFactorInstance::sample_other(), + ]), + ) + } + fn sample_other() -> Self { + Self::new( + IndexSet::just(HDSignature::sample_other()), + IndexSet::just(NeglectedFactorInstance::sample_other()), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + type Sut = PetitionForFactorsStateSnapshot; + + #[test] + fn equality() { + assert_eq!(Sut::sample(), Sut::sample()); + assert_eq!(Sut::sample_other(), Sut::sample_other()); + } + + #[test] + fn inequality() { + assert_ne!(Sut::sample(), Sut::sample_other()) + } + + #[test] + fn debug() { + assert!(!format!("{:?}", Sut::sample()).is_empty()); + } +} diff --git a/crates/sargon/src/signing/petition_types/petition_for_factors_types/petition_for_factors/petition_for_factors_status.rs b/crates/sargon/src/signing/petition_types/petition_for_factors_types/petition_for_factors/petition_for_factors_status.rs new file mode 100644 index 000000000..6a7e81259 --- /dev/null +++ b/crates/sargon/src/signing/petition_types/petition_for_factors_types/petition_for_factors/petition_for_factors_status.rs @@ -0,0 +1,119 @@ +/// The status of building using a certain list of factors, e.g. threshold or +/// override factors list. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum PetitionForFactorsStatus { + /// In progress, still gathering output from factors (signatures or public keys). + InProgress, + + /// Finished building with factors, either successfully or failed. + Finished(PetitionFactorsStatusFinished), +} + +/// Finished building with factors, either successfully or failed. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum PetitionFactorsStatusFinished { + /// Successful completion of building with factors. + Success, + + /// Failure building with factors, either a simulated status, as in what + /// would happen if we skipped a factor source, or a real failure, as in, + /// the user explicitly chose to skip a factor source even though she was + /// advised it would result in some transaction failing. Or we failed to + /// use a required factor source for what some reason. + Fail, +} + +impl PetitionForFactorsStatus { + /// Reduces / aggergates a list of `PetitionForFactorsStatus` into some + /// other status, e.g. `PetitionsStatus`. + pub(crate) fn aggregate( + statuses: impl IntoIterator, + valid: T, + invalid: T, + pending: T, + ) -> T { + let statuses = statuses.into_iter().collect::>(); + + let are_all_valid = statuses.iter().all(|s| { + matches!( + s, + PetitionForFactorsStatus::Finished( + PetitionFactorsStatusFinished::Success + ) + ) + }); + + if are_all_valid { + return valid; + } + + let is_some_invalid = statuses.iter().any(|s| { + matches!( + s, + PetitionForFactorsStatus::Finished( + PetitionFactorsStatusFinished::Fail + ) + ) + }); + + if is_some_invalid { + return invalid; + } + + pending + } +} + +#[cfg(test)] +mod tests { + use super::*; + + type Sut = PetitionForFactorsStatus; + use super::PetitionFactorsStatusFinished::*; + use super::PetitionForFactorsStatus::*; + + #[test] + fn aggregate_invalid() { + let invalid = Some(1); + let irrelevant = None; + assert_eq!( + Sut::aggregate( + vec![InProgress, Finished(Fail), Finished(Success)], + irrelevant, + invalid, + irrelevant + ), + invalid + ) + } + + #[test] + fn aggregate_pending() { + let pending = Some(1); + let irrelevant = None; + assert_eq!( + Sut::aggregate( + vec![InProgress, Finished(Success), Finished(Success)], + irrelevant, + irrelevant, + pending, + ), + pending + ) + } + + #[test] + fn aggregate_valid() { + let valid = Some(1); + let irrelevant = None; + assert_eq!( + Sut::aggregate( + vec![Finished(Success), Finished(Success)], + valid, + irrelevant, + irrelevant + ), + valid + ) + } +} diff --git a/crates/sargon/src/signing/petition_types/petition_for_factors_types/petition_for_factors/petition_for_factors_sub_state.rs b/crates/sargon/src/signing/petition_types/petition_for_factors_types/petition_for_factors/petition_for_factors_sub_state.rs new file mode 100644 index 000000000..0e2c7f240 --- /dev/null +++ b/crates/sargon/src/signing/petition_types/petition_for_factors_types/petition_for_factors/petition_for_factors_sub_state.rs @@ -0,0 +1,39 @@ +use crate::prelude::*; + +/// A sub-state of `PetitionForFactorsState` which can be used to track factors +/// that have signed or skipped. +#[derive(Clone, PartialEq, Eq, derive_more::Debug)] +#[debug("[{}]", factors.borrow().clone().into_iter().map(|f| format!("{:?}", f)).join(", "))] +pub(crate) struct PetitionForFactorsSubState +where + F: FactorSourceReferencing + Debug, +{ + /// Factors that have signed or skipped + factors: RefCell>, +} + +impl PetitionForFactorsSubState { + pub(super) fn new() -> Self { + Self { + factors: RefCell::new(IndexSet::new()), + } + } + + pub(super) fn insert(&self, factor: &F) { + self.factors.borrow_mut().insert(factor.clone()); + } + + pub(super) fn snapshot(&self) -> IndexSet { + self.factors.borrow().clone() + } + + pub(super) fn references_factor_source_by_id( + &self, + factor_source_id: FactorSourceIDFromHash, + ) -> bool { + self.factors + .borrow() + .iter() + .any(|sf| sf.factor_source_id() == factor_source_id) + } +} diff --git a/crates/sargon/src/signing/petition_types/petition_for_transaction.rs b/crates/sargon/src/signing/petition_types/petition_for_transaction.rs new file mode 100644 index 000000000..deca8b676 --- /dev/null +++ b/crates/sargon/src/signing/petition_types/petition_for_transaction.rs @@ -0,0 +1,349 @@ +use crate::prelude::*; + +/// Petition of signatures for a transaction. +/// Essentially a wrapper around `Iterator`. +#[derive(derive_more::Debug, PartialEq, Eq)] +#[debug("{}", self.debug_str())] +pub(crate) struct PetitionForTransaction { + /// Hash of transaction to sign + pub(crate) intent_hash: IntentHash, + + pub(crate) for_entities: + RefCell>, +} + +impl PetitionForTransaction { + pub(crate) fn new( + intent_hash: IntentHash, + for_entities: HashMap, + ) -> Self { + Self { + intent_hash, + for_entities: RefCell::new(for_entities), + } + } + + /// Returns `(true, _)` if this transaction has been successfully signed by + /// all required factor instances. + /// + /// Returns `(false, _)` if not enough factor instances have signed. + /// + /// The second value in the tuple `(_, IndexSet, _)` contains all + /// the signatures, even if it the transaction was failed, all signatures + /// will be returned (which might be empty). + /// + /// The third value in the tuple `(_, _, IndexSet)` contains the + /// id of all the factor sources which was skipped. + pub(crate) fn outcome(self) -> PetitionTransactionOutcome { + let for_entities = self + .for_entities + .into_inner() + .values() + .map(|x| x.to_owned()) + .collect_vec(); + + let transaction_valid = for_entities + .iter() + .all(|b| b.has_signatures_requirement_been_fulfilled()); + + let signatures = for_entities + .iter() + .flat_map(|x| x.all_signatures()) + .collect::>(); + + let neglected_factors = for_entities + .iter() + .flat_map(|x| x.all_neglected_factor_sources()) + .collect::>(); + + PetitionTransactionOutcome::new( + transaction_valid, + self.intent_hash.clone(), + signatures, + neglected_factors, + ) + } + + pub(crate) fn has_tx_failed(&self) -> bool { + self.for_entities.borrow().values().any(|p| p.has_failed()) + } + + pub(crate) fn all_relevant_factor_instances_of_source( + &self, + factor_source_id: &FactorSourceIDFromHash, + ) -> IndexSet { + assert!(!self.has_tx_failed()); + self.for_entities + .borrow() + .values() + .filter(|&p| { + if p.has_failed() { + debug!("OMITTING petition since it HAS failed: {:?}", p); + false + } else { + debug!( + "INCLUDING petition since it has NOT failed: {:?}", + p + ); + true + } + }) + .cloned() + .flat_map(|petition| petition.all_factor_instances()) + .filter(|f| f.factor_source_id() == *factor_source_id) + .collect() + } + + pub(crate) fn add_signature(&self, signature: HDSignature) { + let for_entities = self.for_entities.borrow_mut(); + let for_entity = for_entities + .get(&signature.owned_factor_instance().owner) + .expect("Should not have added signature to irrelevant PetitionForTransaction, did you pass the wrong signature to the wrong PetitionForTransaction?"); + for_entity.add_signature(signature.clone()); + } + + pub(crate) fn neglect_factor_source(&self, neglected: NeglectedFactor) { + let mut for_entities = self.for_entities.borrow_mut(); + for petition in for_entities.values_mut() { + petition.neglect_if_referenced(neglected.clone()) + } + } + + pub(crate) fn input_for_interactor( + &self, + factor_source_id: &FactorSourceIDFromHash, + ) -> TransactionSignRequestInput { + assert!(!self.should_neglect_factors_due_to_irrelevant( + IndexSet::just(*factor_source_id) + )); + assert!(!self.has_tx_failed()); + TransactionSignRequestInput::new( + self.intent_hash.clone(), + *factor_source_id, + self.all_relevant_factor_instances_of_source(factor_source_id), + ) + } + + pub(crate) fn status_of_each_petition_for_entity( + &self, + ) -> Vec { + self.for_entities + .borrow() + .values() + .map(|petition| petition.status()) + .collect() + } + + pub(crate) fn invalid_transaction_if_neglected_factors( + &self, + factor_source_ids: IndexSet, + ) -> Option { + if self.has_tx_failed() { + // No need to display already failed tx. + return None; + } + let entities = self + .for_entities + .borrow() + .iter() + .filter_map(|(_, petition)| { + petition.invalid_transaction_if_neglected_factors( + factor_source_ids.clone(), + ) + }) + .collect_vec(); + + if entities.is_empty() { + return None; + } + + Some(InvalidTransactionIfNeglected::new( + self.intent_hash.clone(), + entities, + )) + } + + pub(crate) fn should_neglect_factors_due_to_irrelevant( + &self, + factor_source_ids: IndexSet, + ) -> bool { + self.for_entities + .borrow() + .values() + .filter(|&p| p.references_any_factor_source(&factor_source_ids)) + .cloned() + .all(|petition| { + petition.should_neglect_factors_due_to_irrelevant( + factor_source_ids.clone(), + ) + }) + } + + #[allow(unused)] + fn debug_str(&self) -> String { + let entities = self + .for_entities + .borrow() + .iter() + .map(|p| format!("PetitionForEntity({:#?})", p.1)) + .join(", "); + + format!("PetitionForTransaction(for_entities: [{}])", entities) + } +} + +impl HasSampleValues for PetitionForTransaction { + fn sample() -> Self { + let intent_hash = IntentHash::sample(); + let entity = Account::sample_securified_mainnet( + "Grace", + AccountAddress::sample_other(), + || { + GeneralRoleWithHierarchicalDeterministicFactorInstances::r6( + HierarchicalDeterministicFactorInstance::sample_id_to_instance( + CAP26EntityKind::Account, + HDPathComponent::from(6) + ) + ) + }, + ); + Self::new( + intent_hash.clone(), + HashMap::just(( + AddressOfAccountOrPersona::from(entity.address), + PetitionForEntity::new( + intent_hash.clone(), + AddressOfAccountOrPersona::from(entity.address), + PetitionForFactors::sample(), + PetitionForFactors::sample_other(), + ), + )), + ) + } + + fn sample_other() -> Self { + let intent_hash = IntentHash::sample_other(); + let entity = Persona::sample_unsecurified_mainnet( + "Sample Unsec", + HierarchicalDeterministicFactorInstance::sample_fii0(), + ); + Self::new( + intent_hash.clone(), + HashMap::just(( + AddressOfAccountOrPersona::Identity(entity.address), + PetitionForEntity::new( + intent_hash.clone(), + AddressOfAccountOrPersona::Identity(entity.address), + PetitionForFactors::sample_other(), + None, + ), + )), + ) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + type Sut = PetitionForTransaction; + + #[test] + fn equality() { + assert_eq!(Sut::sample(), Sut::sample()); + assert_eq!(Sut::sample_other(), Sut::sample_other()); + } + + #[test] + fn inequality() { + assert_ne!(Sut::sample(), Sut::sample_other()); + } + + #[test] + fn debug() { + assert!(!format!("{:?}", Sut::sample()).is_empty()); + } + + #[test] + fn all_relevant_factor_instances_of_source_ok() { + let intent_hash = IntentHash::sample(); + + let account = Account::sample_at(5); + let matrix = match account.security_state { + EntitySecurityState::Securified { value } => { + value.security_structure.matrix_of_factors.clone() + } + _ => panic!(), + }; + let petition = PetitionForEntity::new_securified( + intent_hash.clone(), + AddressOfAccountOrPersona::from(account.address), + GeneralRoleWithHierarchicalDeterministicFactorInstances::try_from( + (matrix, RoleKind::Primary), + ) + .unwrap(), + ); + + let sut = Sut::new( + IntentHash::sample(), + HashMap::just(( + AddressOfAccountOrPersona::from(account.address), + petition, + )), + ); + sut.neglect_factor_source(NeglectedFactor::new( + NeglectFactorReason::Failure, + FactorSourceIDFromHash::sample_at(1), + )); + + assert_eq!( + sut.all_relevant_factor_instances_of_source( + &FactorSourceIDFromHash::sample_at(4) + ) + .len(), + 1 + ); + } + + #[test] + #[should_panic] + fn all_relevant_factor_instances_of_source_panics_if_invalid() { + let intent_hash = IntentHash::sample(); + + let account = Account::sample_at(5); + let matrix = match account.security_state { + EntitySecurityState::Securified { value } => { + value.security_structure.matrix_of_factors.clone() + } + _ => panic!(), + }; + let petition = PetitionForEntity::new_securified( + intent_hash.clone(), + AddressOfAccountOrPersona::from(account.address), + GeneralRoleWithHierarchicalDeterministicFactorInstances::try_from( + (matrix, RoleKind::Primary), + ) + .unwrap(), + ); + + let sut = Sut::new( + IntentHash::sample(), + HashMap::just(( + AddressOfAccountOrPersona::from(account.address), + petition, + )), + ); + sut.neglect_factor_source(NeglectedFactor::new( + NeglectFactorReason::Failure, + FactorSourceIDFromHash::sample_at(1), + )); + sut.neglect_factor_source(NeglectedFactor::new( + NeglectFactorReason::Failure, + FactorSourceIDFromHash::sample_at(4), + )); + let _ = sut.all_relevant_factor_instances_of_source( + &FactorSourceIDFromHash::sample_at(4), + ); + } +} diff --git a/crates/sargon/src/signing/petition_types/petition_status.rs b/crates/sargon/src/signing/petition_types/petition_status.rs new file mode 100644 index 000000000..918a381a8 --- /dev/null +++ b/crates/sargon/src/signing/petition_types/petition_status.rs @@ -0,0 +1,39 @@ +use crate::prelude::*; + +/// An aggregation of the status of all petitions for transaction, +/// if all transactions are valid, if some are invalid, if none are invalid +/// (but all are not yet valid). +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(crate) enum PetitionsStatus { + /// All transactions are valid. + AllAreValid, + + /// Some transaction is invalid (one or more), and some might be valid. + SomeIsInvalid, + + /// Not all transactions are valid, but none are invalid. + InProgressNoneInvalid, +} + +impl PetitionsStatus { + /// returns true if all petitions are valid. + pub(crate) fn are_all_valid(&self) -> bool { + matches!(self, Self::AllAreValid) + } + + /// returns true if some petitions are invalid. + pub(crate) fn is_some_invalid(&self) -> bool { + matches!(self, Self::SomeIsInvalid) + } + + pub(crate) fn reducing( + statuses: impl IntoIterator, + ) -> Self { + PetitionForFactorsStatus::aggregate( + statuses.into_iter().collect_vec(), + Self::AllAreValid, + Self::SomeIsInvalid, + Self::InProgressNoneInvalid, + ) + } +} diff --git a/crates/sargon/src/signing/petition_types/petitions.rs b/crates/sargon/src/signing/petition_types/petitions.rs new file mode 100644 index 000000000..587c0a117 --- /dev/null +++ b/crates/sargon/src/signing/petition_types/petitions.rs @@ -0,0 +1,266 @@ +#![allow(clippy::non_canonical_partial_ord_impl)] + +use crate::prelude::*; + +#[derive(derive_more::Debug, PartialEq, Eq)] +#[debug("{}", self.debug_str())] +pub(crate) struct Petitions { + /// Lookup from factor to TXID. + /// + /// + /// The same HDFactorSource might be required by many payloads + /// and per payload might be required by many entities, e.g. transactions + /// `t0` and `t1`, where + /// `t0` is signed by accounts: A and B + /// `t1` is signed by accounts: A, C and D, + /// + /// Where A, B, C and D, all use the factor source, e.g. some arculus + /// card which the user has setup as a factor (source) for all these accounts. + pub(crate) factor_source_to_intent_hashes: + HashMap>, + + /// Lookup from TXID to signatures builders, sorted according to the order of + /// transactions passed to the SignaturesBuilder. + pub(crate) txid_to_petition: + RefCell>, +} + +impl Petitions { + pub(crate) fn new( + factor_source_to_intent_hashes: HashMap< + FactorSourceIDFromHash, + IndexSet, + >, + txid_to_petition: IndexMap, + ) -> Self { + Self { + factor_source_to_intent_hashes, + txid_to_petition: RefCell::new(txid_to_petition), + } + } + + pub(crate) fn outcome(self) -> SignaturesOutcome { + let txid_to_petition = self.txid_to_petition.into_inner(); + let mut failed_transactions = MaybeSignedTransactions::empty(); + let mut successful_transactions = MaybeSignedTransactions::empty(); + let mut neglected_factor_sources = IndexSet::::new(); + for (intent_hash, petition_of_transaction) in + txid_to_petition.into_iter() + { + let outcome = petition_of_transaction.outcome(); + let signatures = outcome.signatures; + + if outcome.transaction_valid { + successful_transactions.add_signatures(intent_hash, signatures); + } else { + failed_transactions.add_signatures(intent_hash, signatures); + } + neglected_factor_sources.extend(outcome.neglected_factors) + } + + SignaturesOutcome::new( + successful_transactions, + failed_transactions, + neglected_factor_sources, + ) + } + + pub(crate) fn each_petition( + &self, + factor_source_ids: IndexSet, + each: impl Fn(&PetitionForTransaction) -> T, + combine: impl Fn(Vec) -> U, + ) -> U { + let for_each = factor_source_ids + .clone() + .iter() + .flat_map(|f| { + self.factor_source_to_intent_hashes + .get(f) + .expect("Should be able to lookup intent hash for each factor source, did you call this method with irrelevant factor sources? Or did you recently change the preprocessor logic of the SignaturesCollector, if you did you've missed adding an entry for `factor_source_to_intent_hash`.map") + .iter() + .map(|intent_hash| { + let binding = self.txid_to_petition.borrow(); + let value = binding.get(intent_hash).expect("Should have a petition for each transaction, did you recently change the preprocessor logic of the SignaturesCollector, if you did you've missed adding an entry for `txid_to_petition`.map"); + each(value) + }) + }).collect_vec(); + combine(for_each) + } + + pub(crate) fn invalid_transactions_if_neglected_factors( + &self, + factor_source_ids: IndexSet, + ) -> IndexSet { + self.each_petition( + factor_source_ids.clone(), + |p| { + p.invalid_transaction_if_neglected_factors( + factor_source_ids.clone(), + ) + }, + |i| i.into_iter().flatten().collect(), + ) + } + + pub(crate) fn should_neglect_factors_due_to_irrelevant( + &self, + factor_sources_of_kind: &FactorSourcesOfKind, + ) -> bool { + let ids = factor_sources_of_kind + .factor_sources() + .iter() + .map(|f| { + *f.factor_source_id().as_hash().expect( + "Signature Collector only works with HD FactorSources.", + ) + }) + .collect::>(); + self.each_petition( + ids.clone(), + |p| p.should_neglect_factors_due_to_irrelevant(ids.clone()), + |i| i.into_iter().all(|x| x), + ) + } + + /// # Panics + /// Panics if no petition deem usage of `FactorSource` with id + /// `factor_source_id` relevant. We SHOULD have checked this already with + /// `should_neglect_factors_due_to_irrelevant` from SignatureCollector main + /// loop, i.e. we should not have called this method from SignaturesCollector + /// if `should_neglect_factors_due_to_irrelevant` returned true. + pub(crate) fn input_for_interactor( + &self, + factor_source_id: &FactorSourceIDFromHash, + ) -> MonoFactorSignRequestInput { + self.each_petition( + IndexSet::just(*factor_source_id), + |p| { + if p.has_tx_failed() { + None + } else { + Some(p.input_for_interactor(factor_source_id)) + } + }, + |i| { + MonoFactorSignRequestInput::new( + *factor_source_id, + i.into_iter().flatten().collect::>(), + ) + }, + ) + } + + pub(crate) fn status(&self) -> PetitionsStatus { + self.each_petition( + self.factor_source_to_intent_hashes + .keys() + .cloned() + .collect(), + |p| p.status_of_each_petition_for_entity(), + |i| PetitionsStatus::reducing(i.into_iter().flatten()), + ) + } + + fn add_signature(&self, signature: &HDSignature) { + let binding = self.txid_to_petition.borrow(); + let petition = binding.get(signature.intent_hash()).expect("Should have a petition for each transaction, did you recently change the preprocessor logic of the SignaturesCollector, if you did you've missed adding an entry for `txid_to_petition`.map"); + petition.add_signature(signature.clone()) + } + + fn neglect_factor_source_with_id(&self, neglected: NeglectedFactor) { + self.each_petition( + IndexSet::just(neglected.factor_source_id()), + |p| p.neglect_factor_source(neglected.clone()), + |_| (), + ) + } + + pub(crate) fn process_batch_response( + &self, + response: SignWithFactorsOutcome, + ) { + match response { + SignWithFactorsOutcome::Signed { + produced_signatures, + } => { + for (k, v) in produced_signatures.signatures.clone().iter() { + info!("Signed with {} (#{} signatures)", k, v.len()); + } + produced_signatures + .signatures + .values() + .flatten() + .for_each(|s| self.add_signature(s)); + } + SignWithFactorsOutcome::Neglected(neglected_factors) => { + let reason = neglected_factors.reason; + for neglected_factor_source_id in + neglected_factors.content.iter() + { + info!("Neglected {}", neglected_factor_source_id); + self.neglect_factor_source_with_id(NeglectedFactor::new( + reason, + *neglected_factor_source_id, + )) + } + } + } + } + + #[allow(unused)] + fn debug_str(&self) -> String { + self.txid_to_petition + .borrow() + .iter() + .map(|p| format!("Petitions({:#?}: {:#?})", p.0, p.1)) + .join(" + ") + } +} + +impl HasSampleValues for Petitions { + fn sample() -> Self { + let p0 = PetitionForTransaction::sample(); + Self::new( + HashMap::just(( + FactorSourceIDFromHash::sample_at(0), + IndexSet::just(p0.intent_hash.clone()), + )), + IndexMap::just((p0.intent_hash.clone(), p0)), + ) + } + + fn sample_other() -> Self { + let p1 = PetitionForTransaction::sample(); + Self::new( + HashMap::just(( + FactorSourceIDFromHash::sample_at(1), + IndexSet::just(p1.intent_hash.clone()), + )), + IndexMap::just((p1.intent_hash.clone(), p1)), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + type Sut = Petitions; + + #[test] + fn equality_of_samples() { + assert_eq!(Sut::sample(), Sut::sample()); + assert_eq!(Sut::sample_other(), Sut::sample_other()); + } + + #[test] + fn inequality_of_samples() { + assert_ne!(Sut::sample(), Sut::sample_other()); + } + + #[test] + fn debug() { + assert!(!format!("{:?}", Sut::sample()).is_empty()); + } +} diff --git a/crates/sargon/src/signing/petition_types/role_kind.rs b/crates/sargon/src/signing/petition_types/role_kind.rs new file mode 100644 index 000000000..b2a23a737 --- /dev/null +++ b/crates/sargon/src/signing/petition_types/role_kind.rs @@ -0,0 +1,6 @@ +#[derive(Debug, Clone, Copy)] +pub enum RoleKind { + Primary, + Recovery, + Confirmation, +} diff --git a/crates/sargon/src/signing/signatures_outecome_types/maybe_signed_transactions.rs b/crates/sargon/src/signing/signatures_outecome_types/maybe_signed_transactions.rs new file mode 100644 index 000000000..3906e4af3 --- /dev/null +++ b/crates/sargon/src/signing/signatures_outecome_types/maybe_signed_transactions.rs @@ -0,0 +1,337 @@ +use crate::prelude::*; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub(crate) struct MaybeSignedTransactions { + /// Collection of transactions which might be signed or not. + pub(super) transactions: IndexMap>, +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct SignedTransaction { + /// The transaction intent hash. + pub(crate) intent_hash: IntentHash, + /// The signatures for this transaction. + pub(crate) signatures: IndexSet, +} +impl SignedTransaction { + pub(crate) fn new( + intent_hash: IntentHash, + signatures: IndexSet, + ) -> Self { + Self { + intent_hash, + signatures, + } + } +} + +impl MaybeSignedTransactions { + fn new(transactions: IndexMap>) -> Self { + Self { transactions } + } + + /// Constructs a new empty `MaybeSignedTransactions` which can be used + /// as a "builder". + pub(crate) fn empty() -> Self { + Self::new(IndexMap::new()) + } + + /// Returns whether or not this `MaybeSignedTransactions` contains + /// any transactions. + pub(crate) fn is_empty(&self) -> bool { + self.transactions.is_empty() + } + + pub(crate) fn transactions(&self) -> Vec { + self.transactions + .clone() + .into_iter() + .map(|(k, v)| SignedTransaction::new(k, v)) + .collect_vec() + } + + /// Validates that all values, all signatures, have the same `intent_hash` + /// as its key. + /// + /// Also validates that the input of every signature is unique - to identify + /// if the same signer has been used twice, would be a programmer error. + /// + /// # Panics + /// Panics if any signature has a different `intent_hash` than its key. + fn validate(&self) { + for (intent_hash, signatures) in self.transactions.iter() { + assert!( + signatures.iter().all(|s| s.intent_hash() == intent_hash), + "Discrepancy between intent hash and signature intent hash." + ); + } + let all_signatures = self.all_signatures(); + let all_signatures_count = all_signatures.len(); + let inputs = self + .all_signatures() + .iter() + .map(|s| s.input.clone()) + .collect::>(); + assert_eq!( + all_signatures_count, + inputs.len(), + "Discrepancy, the same signer has been used twice." + ); + } + + /// Inserts a set of signatures for transaction with `intent_hash`, if + /// the transaction was already present, the signatures are added to the + /// existing set, if the transaction was not already present a new set is + /// created. + /// + /// # Panics + /// Panics if any signature has a different `intent_hash` than its key. + /// + /// Panics if any signatures in `signature` is not new, that is, already present + /// in `transactions`. + pub(crate) fn add_signatures( + &mut self, + intent_hash: IntentHash, + signatures: IndexSet, + ) { + if let Some(ref mut sigs) = self.transactions.get_mut(&intent_hash) { + let old_count = sigs.len(); + let delta_count = signatures.len(); + sigs.extend(signatures); + assert_eq!( + sigs.len(), + old_count + delta_count, + "Discrepancy, some signature in signatures to add found in existing set." + ); + } else { + self.transactions.insert(intent_hash, signatures); + } + self.validate(); + } + + /// Returns all the signatures for all the transactions. + pub(crate) fn all_signatures(&self) -> IndexSet { + self.transactions + .values() + .flat_map(|v| v.iter()) + .cloned() + .collect() + } +} + +impl HasSampleValues for MaybeSignedTransactions { + fn sample() -> Self { + let tx_a = IntentHash::sample(); + + let tx_a_input_x = HDSignatureInput::new( + tx_a.clone(), + OwnedFactorInstance::new( + AddressOfAccountOrPersona::sample(), + HierarchicalDeterministicFactorInstance::sample_mainnet_tx_account( + HDPathComponent::from(0), + FactorSourceIDFromHash::sample(), + ), + ), + ); + let tx_a_input_y = HDSignatureInput::new( + tx_a.clone(), + OwnedFactorInstance::new( + AddressOfAccountOrPersona::sample(), + HierarchicalDeterministicFactorInstance::sample_mainnet_tx_account( + HDPathComponent::from(1), + FactorSourceIDFromHash::sample_other(), + ), + ), + ); + let tx_a_sig_x = + HDSignature::fake_sign_by_looking_up_mnemonic_amongst_samples( + tx_a_input_x, + ); + let tx_a_sig_y = + HDSignature::fake_sign_by_looking_up_mnemonic_amongst_samples( + tx_a_input_y, + ); + + let tx_b = IntentHash::sample_other(); + let tx_b_input_x = HDSignatureInput::new( + tx_b.clone(), + OwnedFactorInstance::new( + AddressOfAccountOrPersona::sample(), + HierarchicalDeterministicFactorInstance::sample_mainnet_tx_account( + HDPathComponent::from(2), + FactorSourceIDFromHash::sample_at(3), + ), + ), + ); + let tx_b_input_y = HDSignatureInput::new( + tx_b.clone(), + OwnedFactorInstance::new( + AddressOfAccountOrPersona::sample(), + HierarchicalDeterministicFactorInstance::sample_mainnet_tx_account( + HDPathComponent::from(3), + FactorSourceIDFromHash::sample_at(4), + ), + ), + ); + + let tx_b_sig_x = + HDSignature::fake_sign_by_looking_up_mnemonic_amongst_samples( + tx_b_input_x, + ); + let tx_b_sig_y = + HDSignature::fake_sign_by_looking_up_mnemonic_amongst_samples( + tx_b_input_y, + ); + + Self::new( + [ + (tx_a, IndexSet::from_iter([tx_a_sig_x, tx_a_sig_y])), + (tx_b, IndexSet::from_iter([tx_b_sig_x, tx_b_sig_y])), + ] + .into_iter() + .collect::>>(), + ) + } + + fn sample_other() -> Self { + let tx_a = IntentHash::new(Hash::sample_third(), NetworkID::Mainnet); + + let tx_a_input_x = HDSignatureInput::new( + tx_a.clone(), + OwnedFactorInstance::new( + AddressOfAccountOrPersona::sample(), + HierarchicalDeterministicFactorInstance::sample_mainnet_tx_account( + HDPathComponent::from(10), + FactorSourceIDFromHash::sample(), + ), + ), + ); + let tx_a_input_y = HDSignatureInput::new( + tx_a.clone(), + OwnedFactorInstance::new( + AddressOfAccountOrPersona::sample(), + HierarchicalDeterministicFactorInstance::sample_mainnet_tx_account( + HDPathComponent::from(11), + FactorSourceIDFromHash::sample_other(), + ), + ), + ); + let tx_a_input_z = HDSignatureInput::new( + tx_a.clone(), + OwnedFactorInstance::new( + AddressOfAccountOrPersona::sample(), + HierarchicalDeterministicFactorInstance::sample_mainnet_tx_account( + HDPathComponent::from(12), + FactorSourceIDFromHash::sample_at(3), + ), + ), + ); + let tx_a_sig_x = + HDSignature::fake_sign_by_looking_up_mnemonic_amongst_samples( + tx_a_input_x, + ); + let tx_a_sig_y = + HDSignature::fake_sign_by_looking_up_mnemonic_amongst_samples( + tx_a_input_y, + ); + let tx_a_sig_z = + HDSignature::fake_sign_by_looking_up_mnemonic_amongst_samples( + tx_a_input_z, + ); + + Self::new( + [( + tx_a, + IndexSet::from_iter([tx_a_sig_x, tx_a_sig_y, tx_a_sig_z]), + )] + .into_iter() + .collect::>>(), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + type Sut = MaybeSignedTransactions; + + #[test] + fn equality_of_samples() { + assert_eq!(Sut::sample(), Sut::sample()); + assert_eq!(Sut::sample_other(), Sut::sample_other()); + } + + #[test] + fn inequality_of_samples() { + assert_ne!(Sut::sample(), Sut::sample_other()); + } + + #[test] + #[should_panic( + expected = "Discrepancy, some signature in signatures to add found in existing set." + )] + fn panics_when_adding_same_signature() { + let mut sut = Sut::sample(); + let tx = IntentHash::sample(); + let input = HDSignatureInput::new( + tx.clone(), + OwnedFactorInstance::new( + AddressOfAccountOrPersona::sample(), + HierarchicalDeterministicFactorInstance::sample_mainnet_tx_account( + HDPathComponent::from(0), + FactorSourceIDFromHash::sample(), + ), + ), + ); + let signature = HDSignature::produced_signing_with_input(input); + + sut.add_signatures(tx, IndexSet::from_iter([signature])); + } + + #[test] + #[should_panic( + expected = "Discrepancy between intent hash and signature intent hash." + )] + fn panics_when_intent_hash_key_does_not_match_signature() { + let mut sut = Sut::sample(); + let tx = IntentHash::sample(); + + let input = HDSignatureInput::new( + tx, + OwnedFactorInstance::new( + AddressOfAccountOrPersona::sample(), + HierarchicalDeterministicFactorInstance::sample_mainnet_tx_account( + HDPathComponent::from(0), + FactorSourceIDFromHash::sample(), + ), + ), + ); + let signature = HDSignature::produced_signing_with_input(input); + + sut.add_signatures( + IntentHash::sample_other(), + IndexSet::from_iter([signature]), + ); + } + + #[test] + #[should_panic( + expected = "Discrepancy, the same signer has been used twice." + )] + fn panics_when_same_signer_used_twice() { + let mut sut = Sut::empty(); + let factor_instance = OwnedFactorInstance::sample(); + let tx = IntentHash::sample(); + let input = HDSignatureInput::new(tx.clone(), factor_instance.clone()); + let sig_a = HDSignature { + input: input.clone(), + signature: Signature::sample(), + }; + let sig_b = HDSignature { + input: input.clone(), + signature: Signature::sample_other(), + }; + sut.add_signatures(tx, IndexSet::from_iter([sig_a, sig_b])); + } +} diff --git a/crates/sargon/src/signing/signatures_outecome_types/mod.rs b/crates/sargon/src/signing/signatures_outecome_types/mod.rs new file mode 100644 index 000000000..bcabf423a --- /dev/null +++ b/crates/sargon/src/signing/signatures_outecome_types/mod.rs @@ -0,0 +1,9 @@ +mod maybe_signed_transactions; +mod petition_transaction_outcome; +mod sign_with_factors_outcome; +mod signatures_outcome; + +pub use maybe_signed_transactions::*; +pub use petition_transaction_outcome::*; +pub use sign_with_factors_outcome::*; +pub use signatures_outcome::*; diff --git a/crates/sargon/src/signing/signatures_outecome_types/petition_transaction_outcome.rs b/crates/sargon/src/signing/signatures_outecome_types/petition_transaction_outcome.rs new file mode 100644 index 000000000..15d3af697 --- /dev/null +++ b/crates/sargon/src/signing/signatures_outecome_types/petition_transaction_outcome.rs @@ -0,0 +1,56 @@ +use crate::prelude::*; + +/// The outcome of collecting signatures for a specific +/// transasction - either valid or invalid - and a +/// set of collected signatues (might be empty) and +/// a set of neglected factors (might be empty). +#[derive(Clone, PartialEq, Eq)] +pub(crate) struct PetitionTransactionOutcome { + intent_hash: IntentHash, + pub(crate) transaction_valid: bool, + pub(crate) signatures: IndexSet, + pub(crate) neglected_factors: IndexSet, +} + +impl PetitionTransactionOutcome { + /// # Panics + /// Panics if the intent hash in any signatures does not + /// match `intent_hash` + pub(crate) fn new( + transaction_valid: bool, + intent_hash: IntentHash, + signatures: IndexSet, + neglected_factors: IndexSet, + ) -> Self { + assert!( + signatures.iter().all(|s| *s.intent_hash() == intent_hash), + "Discprenacy! Mismatching intent hash found in a signature." + ); + Self { + intent_hash, + transaction_valid, + signatures, + neglected_factors, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + type Sut = PetitionTransactionOutcome; + + #[test] + #[should_panic( + expected = "Discprenacy! Mismatching intent hash found in a signature." + )] + fn panic() { + Sut::new( + true, + IntentHash::sample(), + IndexSet::just(HDSignature::sample_other()), + IndexSet::new(), + ); + } +} diff --git a/crates/sargon/src/signing/signatures_outecome_types/sign_with_factors_outcome.rs b/crates/sargon/src/signing/signatures_outecome_types/sign_with_factors_outcome.rs new file mode 100644 index 000000000..9ad5327ed --- /dev/null +++ b/crates/sargon/src/signing/signatures_outecome_types/sign_with_factors_outcome.rs @@ -0,0 +1,61 @@ +use crate::prelude::*; + +#[derive(Clone, PartialEq, Eq, derive_more::Debug)] +pub enum SignWithFactorsOutcome { + /// The user successfully signed with the factor source(s), the associated + /// value contains the produces signatures and any relevant metadata. + #[debug("Signed: {:#?}", produced_signatures)] + Signed { produced_signatures: SignResponse }, + + /// The factor source got neglected, either due to user explicitly skipping + /// or due to failure + #[debug("Neglected")] + Neglected(NeglectedFactors), +} + +impl SignWithFactorsOutcome { + #[allow(unused)] + pub fn signed(produced_signatures: SignResponse) -> Self { + Self::Signed { + produced_signatures, + } + } + + #[allow(unused)] + pub(crate) fn failure_with_factors( + ids: IndexSet, + ) -> Self { + Self::Neglected(NeglectedFactors::new( + NeglectFactorReason::Failure, + ids, + )) + } + + #[allow(unused)] + pub(crate) fn user_skipped_factors( + ids: IndexSet, + ) -> Self { + Self::Neglected(NeglectedFactors::new( + NeglectFactorReason::UserExplicitlySkipped, + ids, + )) + } + + #[allow(unused)] + pub(crate) fn user_skipped_factor(id: FactorSourceIDFromHash) -> Self { + Self::user_skipped_factors(IndexSet::from_iter([id])) + } + + pub(crate) fn irrelevant( + factor_sources_of_kind: &FactorSourcesOfKind, + ) -> Self { + Self::Neglected(NeglectedFactors::new( + NeglectFactorReason::Irrelevant, + factor_sources_of_kind + .factor_sources() + .into_iter() + .map(|f| *f.factor_source_id().as_hash().unwrap()) // TODO ask that + .collect(), + )) + } +} diff --git a/crates/sargon/src/signing/signatures_outecome_types/signatures_outcome.rs b/crates/sargon/src/signing/signatures_outecome_types/signatures_outcome.rs new file mode 100644 index 000000000..e293ce132 --- /dev/null +++ b/crates/sargon/src/signing/signatures_outecome_types/signatures_outcome.rs @@ -0,0 +1,182 @@ +use crate::prelude::*; + +/// The outcome of a SignaturesCollector, containing a collection for transactions +/// which would be successful if submitted to the network (from a signatures point of view) +/// and a collection of transactions which would fail if submitted to the network, +/// since not enough signatures have been gathered. And a collection of factor sources +/// which were skipped. +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct SignaturesOutcome { + /// A potentially empty collection of transactions which which would be + /// successful if submitted to the network (from a signatures point of view). + /// + /// Potentially empty + successful_transactions: MaybeSignedTransactions, + + /// A collection of transactions which would fail if submitted to the network, + /// since not enough signatures have been gathered. + /// + /// Potentially empty + failed_transactions: MaybeSignedTransactions, + + /// List of all neglected factor sources, either explicitly skipped by user or + /// implicitly neglected due to failure. + neglected_factor_sources: IndexSet, +} + +impl SignaturesOutcome { + /// # Panics + /// Panics if the `successful_transactions` or `failed_transactions` shared + /// either any transaction intent hash, or any signature. + pub(crate) fn new( + successful_transactions: MaybeSignedTransactions, + failed_transactions: MaybeSignedTransactions, + neglected_factor_sources: impl IntoIterator, + ) -> Self { + let neglected_factor_sources = neglected_factor_sources + .into_iter() + .collect::>(); + + let successful_hashes: IndexSet = successful_transactions + .transactions + .keys() + .cloned() + .collect(); + + let failure_hashes: IndexSet = + failed_transactions.transactions.keys().cloned().collect(); + + let valid = successful_hashes + .intersection(&failure_hashes) + .collect_vec() + .is_empty(); + + assert!( + valid, + "Discrepancy, found intent hash in both successful and failed transactions, this is a programmer error." + ); + + assert!(failed_transactions.is_empty() || !neglected_factor_sources.is_empty(), "Discrepancy, found failed transactions but no neglected factor sources, this is a programmer error."); + + Self { + successful_transactions, + failed_transactions, + neglected_factor_sources, + } + } + + pub fn successful(&self) -> bool { + self.failed_transactions.is_empty() + } + + pub fn signatures_of_successful_transactions( + &self, + ) -> IndexSet { + self.successful_transactions.all_signatures() + } + + pub fn successful_transactions(&self) -> Vec { + self.successful_transactions.clone().transactions() + } + + pub fn failed_transactions(&self) -> Vec { + self.failed_transactions.clone().transactions() + } + + pub fn neglected_factor_sources(&self) -> IndexSet { + self.neglected_factor_sources.clone() + } + + #[allow(unused)] + fn ids_of_neglected_factor_sources_filter( + &self, + filter: fn(&NeglectedFactor) -> bool, + ) -> IndexSet { + self.neglected_factor_sources() + .into_iter() + .filter(filter) + .map(|n| n.factor_source_id()) + .collect() + } + + #[allow(unused)] + pub fn ids_of_neglected_factor_sources( + &self, + ) -> IndexSet { + self.ids_of_neglected_factor_sources_filter(|_| true) + } + + #[allow(unused)] + pub(crate) fn ids_of_neglected_factor_sources_skipped_by_user( + &self, + ) -> IndexSet { + self.ids_of_neglected_factor_sources_filter(|nf| { + nf.reason == NeglectFactorReason::UserExplicitlySkipped + }) + } + + #[allow(unused)] + pub(crate) fn ids_of_neglected_factor_sources_failed( + &self, + ) -> IndexSet { + self.ids_of_neglected_factor_sources_filter(|nf| { + nf.reason == NeglectFactorReason::Failure + }) + } + + #[allow(unused)] + pub(crate) fn ids_of_neglected_factor_sources_irrelevant( + &self, + ) -> IndexSet { + self.ids_of_neglected_factor_sources_filter(|nf| { + nf.reason == NeglectFactorReason::Irrelevant + }) + } + + #[allow(unused)] + pub(crate) fn signatures_of_failed_transactions( + &self, + ) -> IndexSet { + self.failed_transactions.all_signatures() + } + + #[allow(unused)] + /// All signatures from both successful transactions and failed transactions. + pub(crate) fn all_signatures(&self) -> IndexSet { + self.signatures_of_successful_transactions() + .union(&self.signatures_of_failed_transactions()) + .cloned() + .collect() + } +} + +#[cfg(test)] +mod tests { + + use super::*; + type Sut = SignaturesOutcome; + + #[test] + #[should_panic( + expected = "Discrepancy, found intent hash in both successful and failed transactions, this is a programmer error." + )] + fn new_panics_if_intent_hash_is_in_both_failed_and_success_collection() { + Sut::new( + MaybeSignedTransactions::sample(), + MaybeSignedTransactions::sample(), + [], + ); + } + + #[test] + #[should_panic( + expected = "Discrepancy, found failed transactions but no neglected factor sources, this is a programmer error." + )] + fn new_panics_if_failed_tx_is_not_empty_but_neglected_is() { + Sut::new( + MaybeSignedTransactions::empty(), + MaybeSignedTransactions::sample(), + [], + ); + } +} diff --git a/crates/sargon/src/signing/testing/interactors/mod.rs b/crates/sargon/src/signing/testing/interactors/mod.rs new file mode 100644 index 000000000..4d2f81be6 --- /dev/null +++ b/crates/sargon/src/signing/testing/interactors/mod.rs @@ -0,0 +1,10 @@ +#![cfg(test)] +#![allow(unused)] + +mod test_interactor; +mod test_parallel_interactor; +mod test_serial_interactor; + +pub(crate) use test_interactor::*; +pub(crate) use test_parallel_interactor::*; +pub(crate) use test_serial_interactor::*; diff --git a/crates/sargon/src/signing/testing/interactors/test_interactor.rs b/crates/sargon/src/signing/testing/interactors/test_interactor.rs new file mode 100644 index 000000000..b3e57454e --- /dev/null +++ b/crates/sargon/src/signing/testing/interactors/test_interactor.rs @@ -0,0 +1,17 @@ +#![cfg(test)] +#![allow(unused)] + +use crate::prelude::*; + +#[async_trait::async_trait] +pub(crate) trait IsTestInteractor: Sync { + fn simulated_user(&self) -> SimulatedUser; + + fn should_simulate_failure( + &self, + factor_source_ids: IndexSet, + ) -> bool { + self.simulated_user() + .simulate_failure_if_needed(factor_source_ids) + } +} diff --git a/crates/sargon/src/signing/testing/interactors/test_parallel_interactor.rs b/crates/sargon/src/signing/testing/interactors/test_parallel_interactor.rs new file mode 100644 index 000000000..d37b9d6d2 --- /dev/null +++ b/crates/sargon/src/signing/testing/interactors/test_parallel_interactor.rs @@ -0,0 +1,82 @@ +#![cfg(test)] +#![allow(unused)] + +use crate::prelude::*; + +pub(crate) struct TestSigningParallelInteractor { + simulated_user: SimulatedUser, +} + +impl TestSigningParallelInteractor { + pub(crate) fn new(simulated_user: SimulatedUser) -> Self { + Self { simulated_user } + } +} + +#[async_trait::async_trait] +impl IsTestInteractor for TestSigningParallelInteractor { + fn simulated_user(&self) -> SimulatedUser { + self.simulated_user.clone() + } +} + +#[async_trait::async_trait] +impl PolyFactorSignInteractor for TestSigningParallelInteractor { + async fn sign( + &self, + request: PolyFactorSignRequest, + ) -> SignWithFactorsOutcome { + self.simulated_user.spy_on_request_before_handled( + request.clone().factor_source_kind(), + request.clone().invalid_transactions_if_neglected, + ); + let ids = request + .per_factor_source + .keys() + .cloned() + .collect::>(); + + if self.should_simulate_failure(ids.clone()) { + return SignWithFactorsOutcome::failure_with_factors(ids); + } + + match self + .simulated_user + .sign_or_skip(request.invalid_transactions_if_neglected) + { + SigningUserInput::Sign => { + let signatures = request + .per_factor_source + .iter() + .flat_map(|(_, v)| { + v.per_transaction + .iter() + .flat_map(|x| { + x.signature_inputs() + .iter() + .map(|y| HDSignature::produced_signing_with_input(y.clone())) + .collect_vec() + }) + .collect::>() + }) + .collect::>(); + + let signatures = signatures + .into_iter() + .into_group_map_by(|x| x.factor_source_id()); + let response = SignResponse::new( + signatures + .into_iter() + .map(|(k, v)| (k, IndexSet::from_iter(v))) + .collect(), + ); + + SignWithFactorsOutcome::signed(response) + } + + SigningUserInput::Skip => { + SignWithFactorsOutcome::user_skipped_factors(ids) + } + } + } +} diff --git a/crates/sargon/src/signing/testing/interactors/test_serial_interactor.rs b/crates/sargon/src/signing/testing/interactors/test_serial_interactor.rs new file mode 100644 index 000000000..8ece71bed --- /dev/null +++ b/crates/sargon/src/signing/testing/interactors/test_serial_interactor.rs @@ -0,0 +1,71 @@ +#![cfg(test)] +#![allow(unused)] + +use crate::prelude::*; + +pub(crate) struct TestSigningSerialInteractor { + simulated_user: SimulatedUser, +} + +impl TestSigningSerialInteractor { + pub(crate) fn new(simulated_user: SimulatedUser) -> Self { + Self { simulated_user } + } +} + +#[async_trait::async_trait] +impl IsTestInteractor for TestSigningSerialInteractor { + fn simulated_user(&self) -> SimulatedUser { + self.simulated_user.clone() + } +} + +#[async_trait::async_trait] +impl MonoFactorSignInteractor for TestSigningSerialInteractor { + async fn sign( + &self, + request: MonoFactorSignRequest, + ) -> SignWithFactorsOutcome { + self.simulated_user.spy_on_request_before_handled( + request.clone().factor_source_kind(), + request.clone().invalid_transactions_if_neglected, + ); + let ids = IndexSet::from_iter([request.clone().input.factor_source_id]); + if self.should_simulate_failure(ids.clone()) { + return SignWithFactorsOutcome::failure_with_factors(ids); + } + let invalid_transactions_if_neglected = + request.clone().invalid_transactions_if_neglected; + + match self + .simulated_user + .sign_or_skip(invalid_transactions_if_neglected) + { + SigningUserInput::Sign => { + let signatures = request + .input + .per_transaction + .into_iter() + .flat_map(|r| { + r.signature_inputs() + .iter() + .map(|x| { + HDSignature::produced_signing_with_input( + x.clone(), + ) + }) + .collect::>() + }) + .collect::>(); + SignWithFactorsOutcome::signed(SignResponse::with_signatures( + signatures, + )) + } + SigningUserInput::Skip => { + SignWithFactorsOutcome::user_skipped_factor( + request.input.factor_source_id, + ) + } + } + } +} diff --git a/crates/sargon/src/signing/testing/mod.rs b/crates/sargon/src/signing/testing/mod.rs new file mode 100644 index 000000000..edf355cdd --- /dev/null +++ b/crates/sargon/src/signing/testing/mod.rs @@ -0,0 +1,13 @@ +#![cfg(test)] +#![allow(unused)] +#![allow(unused_imports)] + +mod interactors; +mod simulated_user; +mod test_signature_collecting_interactors; +mod test_signatures_collector; + +pub(crate) use interactors::*; +pub(crate) use simulated_user::*; +pub(crate) use test_signature_collecting_interactors::*; +pub(crate) use test_signatures_collector::*; diff --git a/crates/sargon/src/signing/testing/simulated_user.rs b/crates/sargon/src/signing/testing/simulated_user.rs new file mode 100644 index 000000000..efdde9154 --- /dev/null +++ b/crates/sargon/src/signing/testing/simulated_user.rs @@ -0,0 +1,190 @@ +#![cfg(test)] +#![allow(unused)] + +use crate::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) enum SigningUserInput { + Sign, + Skip, +} + +#[derive(Clone, derive_more::Debug)] +#[debug("SimulatedUser(mode: {mode:?}, failures: {failures:?})")] +pub(crate) struct SimulatedUser { + spy_on_request: + Arc)>, + mode: SimulatedUserMode, + /// `None` means never failures + failures: Option, +} + +impl SimulatedUser { + pub(crate) fn with_spy( + spy_on_request: impl Fn(FactorSourceKind, IndexSet) + + 'static, + mode: SimulatedUserMode, + failures: impl Into>, + ) -> Self { + Self { + spy_on_request: Arc::new(spy_on_request), + mode, + failures: failures.into(), + } + } + pub(crate) fn new( + mode: SimulatedUserMode, + failures: impl Into>, + ) -> Self { + Self::with_spy(|_, _| {}, mode, failures) + } +} + +#[derive(Debug, Clone, Default)] +pub(crate) struct SimulatedFailures { + /// Set of FactorSources which should always fail. + simulated_failures: IndexSet, +} +impl SimulatedFailures { + pub(crate) fn with_details( + simulated_failures: IndexSet, + ) -> Self { + Self { simulated_failures } + } + + pub(crate) fn with_simulated_failures( + failures: impl IntoIterator, + ) -> Self { + Self::with_details(IndexSet::from_iter(failures)) + } + + /// If needed, simulates failure for ALL factor sources or NONE. + pub(crate) fn simulate_failure_if_needed( + &self, + factor_source_ids: IndexSet, + ) -> bool { + factor_source_ids + .into_iter() + .all(|id| self.simulated_failures.contains(&id)) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) enum SimulatedUserMode { + /// Emulation of a "prudent" user, that signs with all factors sources, i.e. + /// she never ever "skips" a factor source + Prudent, + + /// Emulation of a "lazy" user, that skips signing with as many factor + /// sources as possible. + Lazy(Laziness), +} + +impl SimulatedUserMode { + pub(crate) fn lazy_always_skip() -> Self { + Self::Lazy(Laziness::AlwaysSkip) + } + + /// Skips only if `invalid_tx_if_skipped` is empty + pub(crate) fn lazy_sign_minimum() -> Self { + Self::Lazy(Laziness::SignMinimum) + } +} + +impl SimulatedUser { + pub(crate) fn prudent_no_fail() -> Self { + Self::new(SimulatedUserMode::Prudent, None) + } + + pub(crate) fn prudent_with_failures( + simulated_failures: SimulatedFailures, + ) -> Self { + Self::new(SimulatedUserMode::Prudent, simulated_failures) + } + + pub(crate) fn lazy_always_skip_no_fail() -> Self { + Self::new(SimulatedUserMode::lazy_always_skip(), None) + } + + /// Skips only if `invalid_tx_if_skipped` is empty + /// (or if simulated failure for that factor source) + pub(crate) fn lazy_sign_minimum( + simulated_failures: impl IntoIterator, + ) -> Self { + Self::new( + SimulatedUserMode::lazy_sign_minimum(), + SimulatedFailures::with_simulated_failures(simulated_failures), + ) + } +} + +unsafe impl Sync for SimulatedUser {} +unsafe impl Send for SimulatedUser {} + +/// A very lazy user that defers all boring work such as signing stuff for as long +/// as possible. Ironically, this sometimes leads to user signing more than she +/// actually needs. For example, if the user has a Securified Account with threshold +/// and override factors, she actually needs to sign with a single override +/// factor. But since user is so lazy, she defers signing with that override +/// factor if prompted for it first. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) enum Laziness { + SignMinimum, + AlwaysSkip, +} + +impl SimulatedUser { + pub(crate) fn spy_on_request_before_handled( + &self, + factor_source_kind: FactorSourceKind, + invalid_tx_if_skipped: IndexSet, + ) { + (self.spy_on_request)( + factor_source_kind, + invalid_tx_if_skipped.clone(), + ); + } + + pub(crate) fn sign_or_skip( + &self, + invalid_tx_if_skipped: impl IntoIterator< + Item = InvalidTransactionIfNeglected, + >, + ) -> SigningUserInput { + let invalid_tx_if_skipped = invalid_tx_if_skipped + .into_iter() + .collect::>(); + + if self.be_prudent(|| !invalid_tx_if_skipped.is_empty()) { + SigningUserInput::Sign + } else { + SigningUserInput::Skip + } + } + + pub(crate) fn simulate_failure_if_needed( + &self, + factor_source_ids: IndexSet, + ) -> bool { + if let Some(failures) = &self.failures { + failures.simulate_failure_if_needed(factor_source_ids) + } else { + false + } + } + + fn be_prudent(&self, is_prudent: F) -> bool + where + F: Fn() -> bool, + { + use rand::prelude::*; + + match &self.mode { + SimulatedUserMode::Prudent => true, + SimulatedUserMode::Lazy(laziness) => match laziness { + Laziness::AlwaysSkip => false, + Laziness::SignMinimum => is_prudent(), + }, + } + } +} diff --git a/crates/sargon/src/signing/testing/test_signature_collecting_interactors.rs b/crates/sargon/src/signing/testing/test_signature_collecting_interactors.rs new file mode 100644 index 000000000..af81b509d --- /dev/null +++ b/crates/sargon/src/signing/testing/test_signature_collecting_interactors.rs @@ -0,0 +1,27 @@ +#![cfg(test)] +#![allow(unused)] + +use crate::prelude::*; + +pub(crate) struct TestSignatureCollectingInteractors { + pub(crate) simulated_user: SimulatedUser, +} + +impl TestSignatureCollectingInteractors { + pub(crate) fn new(simulated_user: SimulatedUser) -> Self { + Self { simulated_user } + } +} + +impl SignInteractors for TestSignatureCollectingInteractors { + fn interactor_for(&self, kind: FactorSourceKind) -> SignInteractor { + match kind { + FactorSourceKind::Device => SignInteractor::poly(Arc::new( + TestSigningParallelInteractor::new(self.simulated_user.clone()), + )), + _ => SignInteractor::mono(Arc::new( + TestSigningSerialInteractor::new(self.simulated_user.clone()), + )), + } + } +} diff --git a/crates/sargon/src/signing/testing/test_signatures_collector.rs b/crates/sargon/src/signing/testing/test_signatures_collector.rs new file mode 100644 index 000000000..59127cff0 --- /dev/null +++ b/crates/sargon/src/signing/testing/test_signatures_collector.rs @@ -0,0 +1,128 @@ +#![cfg(test)] +#![allow(unused)] + +use crate::prelude::*; + +impl SignaturesCollector { + pub(crate) fn new_test_with( + finish_early_strategy: SigningFinishEarlyStrategy, + all_factor_sources_in_profile: IndexSet, + transactions: IndexSet, + interactors: Arc, + role_kind: RoleKind, + ) -> Self { + Self::with( + finish_early_strategy, + all_factor_sources_in_profile, + transactions, + interactors, + role_kind, + ) + } + pub(crate) fn new_test( + finish_early_strategy: SigningFinishEarlyStrategy, + all_factor_sources_in_profile: impl IntoIterator, + transactions: impl IntoIterator, + simulated_user: SimulatedUser, + role_kind: RoleKind, + ) -> Self { + Self::new_test_with( + finish_early_strategy, + all_factor_sources_in_profile.into_iter().collect(), + transactions.into_iter().collect(), + Arc::new(TestSignatureCollectingInteractors::new(simulated_user)), + role_kind, + ) + } + + pub(crate) fn test_prudent_with_factors_and_finish_early( + finish_early_strategy: SigningFinishEarlyStrategy, + all_factor_sources_in_profile: impl IntoIterator, + transactions: impl IntoIterator, + ) -> Self { + Self::new_test( + finish_early_strategy, + all_factor_sources_in_profile, + transactions, + SimulatedUser::prudent_no_fail(), + RoleKind::Primary, + ) + } + + pub(crate) fn test_prudent_with_finish_early( + finish_early_strategy: SigningFinishEarlyStrategy, + transactions: impl IntoIterator, + ) -> Self { + Self::test_prudent_with_factors_and_finish_early( + finish_early_strategy, + FactorSource::sample_all(), + transactions, + ) + } + + pub(crate) fn test_prudent( + transactions: impl IntoIterator, + ) -> Self { + Self::test_prudent_with_finish_early( + SigningFinishEarlyStrategy::default(), + transactions, + ) + } + + pub(crate) fn test_prudent_with_failures( + transactions: impl IntoIterator, + simulated_failures: SimulatedFailures, + ) -> Self { + Self::new_test( + SigningFinishEarlyStrategy::default(), + FactorSource::sample_all(), + transactions, + SimulatedUser::prudent_with_failures(simulated_failures), + RoleKind::Primary, + ) + } + + pub(crate) fn test_lazy_sign_minimum_no_failures_with_factors( + all_factor_sources_in_profile: impl IntoIterator, + transactions: impl IntoIterator, + ) -> Self { + Self::new_test( + SigningFinishEarlyStrategy::default(), + all_factor_sources_in_profile, + transactions, + SimulatedUser::lazy_sign_minimum([]), + RoleKind::Primary, + ) + } + + pub(crate) fn test_lazy_sign_minimum_no_failures( + transactions: impl IntoIterator, + ) -> Self { + Self::test_lazy_sign_minimum_no_failures_with_factors( + FactorSource::sample_all(), + transactions, + ) + } + + pub(crate) fn test_lazy_always_skip_with_factors( + all_factor_sources_in_profile: impl IntoIterator, + transactions: impl IntoIterator, + ) -> Self { + Self::new_test( + SigningFinishEarlyStrategy::default(), + all_factor_sources_in_profile, + transactions, + SimulatedUser::lazy_always_skip_no_fail(), + RoleKind::Primary, + ) + } + + pub(crate) fn test_lazy_always_skip( + transactions: impl IntoIterator, + ) -> Self { + Self::test_lazy_always_skip_with_factors( + FactorSource::sample_all(), + transactions, + ) + } +} diff --git a/crates/sargon/src/signing/tx_to_sign.rs b/crates/sargon/src/signing/tx_to_sign.rs new file mode 100644 index 000000000..148699c27 --- /dev/null +++ b/crates/sargon/src/signing/tx_to_sign.rs @@ -0,0 +1,84 @@ +use crate::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, std::hash::Hash)] +pub(crate) struct TXToSign { + pub(crate) intent_hash: IntentHash, + entities_requiring_auth: Vec, // should be a set but Sets are not `Hash`. +} + +impl TXToSign { + pub(crate) fn with( + intent_hash: IntentHash, + entities_requiring_auth: impl IntoIterator< + Item = impl Into, + >, + ) -> Self { + Self { + intent_hash, + entities_requiring_auth: entities_requiring_auth + .into_iter() + .map(|i| i.into()) + .collect_vec(), + } + } + + pub(crate) fn entities_requiring_auth(&self) -> IndexSet { + self.entities_requiring_auth.clone().into_iter().collect() + } + + pub(crate) fn extracting_from_intent_and_profile( + intent: &TransactionIntent, + profile: &Profile, + ) -> Result { + let intent_hash = intent.intent_hash().clone(); + let summary = intent.manifest_summary(); + let mut entities_requiring_auth: IndexSet = + IndexSet::new(); + + let accounts = summary + .addresses_of_accounts_requiring_auth + .iter() + .map(|a| profile.account_by_address(*a)) + .collect::>>()?; + + entities_requiring_auth.extend( + accounts + .into_iter() + .map(AccountOrPersona::from) + .collect_vec(), + ); + + let personas = summary + .addresses_of_personas_requiring_auth + .into_iter() + .map(|a| profile.persona_by_address(a)) + .collect::>>()?; + + entities_requiring_auth.extend( + personas + .into_iter() + .map(AccountOrPersona::from) + .collect_vec(), + ); + + Ok(Self::with(intent_hash, entities_requiring_auth)) + } +} + +// -- Samples +impl TXToSign { + #[allow(unused)] + pub(crate) fn sample( + entities_requiring_auth: impl IntoIterator< + Item = impl Into, + >, + ) -> Self { + Self::with( + IntentHash::new( + Hash::from(Exactly32Bytes::generate()), + NetworkID::Mainnet, + ), + entities_requiring_auth, + ) + } +} diff --git a/crates/sargon/src/system/sargon_os/profile_state_holder.rs b/crates/sargon/src/system/sargon_os/profile_state_holder.rs index 1787a74cc..bcb38b36f 100644 --- a/crates/sargon/src/system/sargon_os/profile_state_holder.rs +++ b/crates/sargon/src/system/sargon_os/profile_state_holder.rs @@ -53,7 +53,7 @@ impl ProfileStateHolder { } pub fn current_network(&self) -> Result { - self.try_access_profile_with(|p| p.current_network().map(|n| n.clone())) + self.try_access_profile_with(|p| p.current_network().cloned()) } /// Returns the non-hidden accounts on the current network, empty if no accounts @@ -339,7 +339,7 @@ mod tests { profile.networks.try_update_with( &NetworkID::Mainnet, |network| { - let _res = network + network .accounts .try_insert_unique( Account::sample_mainnet_carol(), diff --git a/crates/sargon/src/types/factor_sources_of_kind.rs b/crates/sargon/src/types/factor_sources_of_kind.rs new file mode 100644 index 000000000..9ec537dbd --- /dev/null +++ b/crates/sargon/src/types/factor_sources_of_kind.rs @@ -0,0 +1,117 @@ +use crate::prelude::*; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct FactorSourcesOfKind { + pub(crate) kind: FactorSourceKind, + factor_sources: Vec, +} + +impl FactorSourcesOfKind { + pub(crate) fn new( + kind: FactorSourceKind, + factor_sources: impl IntoIterator, + ) -> Result { + let factor_sources = + factor_sources.into_iter().collect::>(); + if factor_sources.is_empty() { + return Err(CommonError::FactorSourcesOfKindEmptyFactors); + } + + if let Some(factor_source) = factor_sources + .iter() + .find(|f| f.factor_source_kind() != kind) + { + return Err(CommonError::InvalidFactorSourceKind { + bad_value: factor_source.factor_source_kind().to_string(), + }); + } + + Ok(Self { + kind, + factor_sources: factor_sources.into_iter().collect(), + }) + } + + pub(crate) fn factor_sources(&self) -> IndexSet { + self.factor_sources.clone().into_iter().collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + type Sut = FactorSourcesOfKind; + + #[test] + fn invalid_empty() { + assert_eq!( + Sut::new(FactorSourceKind::Device, []), + Err(CommonError::FactorSourcesOfKindEmptyFactors) + ); + } + + #[test] + fn invalid_single_element() { + assert_eq!( + Sut::new( + FactorSourceKind::Device, + [FactorSource::sample_arculus()] + ), + Err(CommonError::InvalidFactorSourceKind { + bad_value: FactorSourceKind::ArculusCard.to_string() + }) + ); + } + + #[test] + fn invalid_two_two() { + assert_eq!( + Sut::new( + FactorSourceKind::Device, + [ + FactorSource::sample_arculus(), + FactorSource::sample_device_babylon(), + FactorSource::sample_arculus_other(), + FactorSource::sample_device_babylon_other() + ] + ), + Err(CommonError::InvalidFactorSourceKind { + bad_value: FactorSourceKind::ArculusCard.to_string() + }) + ); + } + + #[test] + fn valid_one() { + let sources = + IndexSet::::just(FactorSource::sample_device()); + let sut = Sut::new(FactorSourceKind::Device, sources.clone()).unwrap(); + assert_eq!(sut.factor_sources(), sources); + } + + #[test] + fn valid_two() { + let sources = IndexSet::::from_iter([ + FactorSource::sample_ledger(), + FactorSource::sample_ledger_other(), + ]); + let sut = + Sut::new(FactorSourceKind::LedgerHQHardwareWallet, sources.clone()) + .unwrap(); + assert_eq!(sut.factor_sources(), sources); + assert_eq!(sut.factor_sources().len(), 2); + } + + #[test] + fn valid_no_duplicates() { + let sources = IndexSet::::from_iter([ + FactorSource::sample_ledger(), + FactorSource::sample_ledger(), + ]); + let sut = + Sut::new(FactorSourceKind::LedgerHQHardwareWallet, sources.clone()) + .unwrap(); + assert_eq!(sut.factor_sources(), sources); + assert_eq!(sut.factor_sources().len(), 1); + } +} diff --git a/crates/sargon/src/types/hd_signature.rs b/crates/sargon/src/types/hd_signature.rs new file mode 100644 index 000000000..a7611b3b3 --- /dev/null +++ b/crates/sargon/src/types/hd_signature.rs @@ -0,0 +1,119 @@ +use crate::prelude::*; + +/// A signature of `intent_hash` by `entity` using `factor_source_id` and `derivation_path`, with `public_key` used for verification. +#[derive(Clone, PartialEq, Eq, Hash, derive_more::Debug)] +#[debug("HDSignature {{ input: {:#?} }}", input)] +pub struct HDSignature { + /// The input used to produce this `HDSignature` + pub input: HDSignatureInput, + + /// The ECDSA/EdDSA signature produced by the private key of the + /// `owned_hd_factor_instance.public_key`, + /// derived by the HDFactorSource identified by + /// `owned_hd_factor_ + /// instance.factor_s + /// ource_id` and which + /// was derived at `owned_hd_factor_instance.derivation_path`. + pub signature: Signature, +} + +impl HDSignature { + /// Constructs a HDSignature from an already produced `Signature`. + pub fn with_details(input: HDSignatureInput, signature: Signature) -> Self { + Self { input, signature } + } + + pub fn intent_hash(&self) -> &IntentHash { + &self.input.intent_hash + } + + pub fn owned_factor_instance(&self) -> &OwnedFactorInstance { + &self.input.owned_factor_instance + } + + pub fn factor_source_id(&self) -> FactorSourceIDFromHash { + self.owned_factor_instance() + .factor_instance() + .factor_source_id + } + + pub fn derivation_path(&self) -> DerivationPath { + self.input + .owned_factor_instance + .factor_instance() + .derivation_path() + } +} + +impl HasSampleValues for HDSignature { + fn sample() -> Self { + Self::fake_sign_by_looking_up_mnemonic_amongst_samples( + HDSignatureInput::sample(), + ) + } + + fn sample_other() -> Self { + Self::fake_sign_by_looking_up_mnemonic_amongst_samples( + HDSignatureInput::sample_other(), + ) + } +} + +impl HDSignature { + /// WARNING: Should only be used in samples and unit tests + /// + /// Signs with predefined mnemonics associated to the input's factor source id + pub fn fake_sign_by_looking_up_mnemonic_amongst_samples( + input: HDSignatureInput, + ) -> Self { + let id = input.owned_factor_instance.factor_source_id(); + + let mnemonic_with_passphrase = id.sample_associated_mnemonic(); + + let signature = mnemonic_with_passphrase.sign( + &input.intent_hash.hash, + &input.owned_factor_instance.value.public_key.derivation_path, + ); + + HDSignature::with_details(input, signature.signature()) + } +} + +#[cfg(test)] +impl HDSignature { + pub fn produced_signing_with_input(input: HDSignatureInput) -> Self { + Self::fake_sign_by_looking_up_mnemonic_amongst_samples(input) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + type Sut = HDSignature; + + #[test] + fn equality_of_samples() { + assert_eq!(Sut::sample(), Sut::sample()); + assert_eq!(Sut::sample_other(), Sut::sample_other()); + } + + #[test] + fn inequality_of_samples() { + assert_ne!(Sut::sample(), Sut::sample_other()); + } + + #[test] + fn hash_of_samples() { + assert_eq!( + IndexSet::::from_iter([ + Sut::sample(), + Sut::sample_other(), + Sut::sample(), + Sut::sample_other() + ]) + .len(), + 2 + ); + } +} diff --git a/crates/sargon/src/types/hd_signature_input.rs b/crates/sargon/src/types/hd_signature_input.rs new file mode 100644 index 000000000..ce1b70c29 --- /dev/null +++ b/crates/sargon/src/types/hd_signature_input.rs @@ -0,0 +1,75 @@ +use crate::prelude::*; + +/// The input used to produce a `HDSignature`. Can be used to see two signatures +/// has the same signer, which would be a bug. +#[derive(Clone, PartialEq, Eq, Hash, derive_more::Debug)] +#[debug( + "HDSignatureInput {{ intent_hash: {:#?}, owned_factor_instance: {:#?} }}", + intent_hash, + owned_factor_instance +)] +pub struct HDSignatureInput { + /// Hash which was signed. + pub intent_hash: IntentHash, + + /// The account or identity address of the entity which signed the hash, + /// with expected public key and with derivation path to derive PrivateKey + /// with. + pub owned_factor_instance: OwnedFactorInstance, +} +impl HDSignatureInput { + /// Constructs a new `HDSignatureInput`. + pub fn new( + intent_hash: IntentHash, + owned_factor_instance: OwnedFactorInstance, + ) -> Self { + Self { + intent_hash, + owned_factor_instance, + } + } +} + +impl HasSampleValues for HDSignatureInput { + fn sample() -> Self { + Self::new(IntentHash::sample(), OwnedFactorInstance::sample()) + } + fn sample_other() -> Self { + Self::new( + IntentHash::sample_other(), + OwnedFactorInstance::sample_other(), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + type Sut = HDSignatureInput; + + #[test] + fn equality_of_samples() { + assert_eq!(Sut::sample(), Sut::sample()); + assert_eq!(Sut::sample_other(), Sut::sample_other()); + } + + #[test] + fn inequality_of_samples() { + assert_ne!(Sut::sample(), Sut::sample_other()); + } + + #[test] + fn hash_of_samples() { + assert_eq!( + IndexSet::::from_iter([ + Sut::sample(), + Sut::sample_other(), + Sut::sample(), + Sut::sample_other() + ]) + .len(), + 2 + ); + } +} diff --git a/crates/sargon/src/types/invalid_transaction_if_neglected.rs b/crates/sargon/src/types/invalid_transaction_if_neglected.rs new file mode 100644 index 000000000..efa024534 --- /dev/null +++ b/crates/sargon/src/types/invalid_transaction_if_neglected.rs @@ -0,0 +1,98 @@ +use crate::prelude::*; + +/// A list of entities which would fail in a transaction if we would +/// neglect certain factor source, either by user explicitly skipping +/// it or if implicitly neglected due to failure. +#[derive(Clone, Debug, PartialEq, Eq, std::hash::Hash)] +pub struct InvalidTransactionIfNeglected { + /// The intent hash of the transaction which would be invalid if a + /// certain factor source would be neglected, either if user + /// explicitly skipped it or implicitly neglected due to failure. + pub intent_hash: IntentHash, + + /// The entities in the transaction which would fail auth. + entities_which_would_fail_auth: Vec, +} + +impl InvalidTransactionIfNeglected { + /// Constructs a new `InvalidTransactionIfNeglected` from an IndexSet of + /// entities which would fail auth.. + /// + /// # Panics + /// Panics if `entities_which_would_fail_auth` is empty. + pub fn new( + intent_hash: IntentHash, + entities_which_would_fail_auth: impl IntoIterator< + Item = AddressOfAccountOrPersona, + >, + ) -> Self { + let entities_which_would_fail_auth = + entities_which_would_fail_auth.into_iter().collect_vec(); + let len = entities_which_would_fail_auth.len(); + let entities_which_would_fail_auth = entities_which_would_fail_auth + .into_iter() + .collect::>(); + + assert!(!entities_which_would_fail_auth.is_empty(), "'entities_which_would_fail_auth' must not be empty, this type is not useful if it is empty."); + + assert_eq!( + entities_which_would_fail_auth.len(), + len, + "entities_which_would_fail_auth must not contain duplicates." + ); + + Self { + intent_hash, + entities_which_would_fail_auth: entities_which_would_fail_auth + .into_iter() + .collect_vec(), + } + } + + pub fn entities_which_would_fail_auth( + &self, + ) -> IndexSet { + IndexSet::from_iter(self.entities_which_would_fail_auth.clone()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + type Sut = InvalidTransactionIfNeglected; + + #[test] + #[should_panic( + expected = "'entities_which_would_fail_auth' must not be empty, this type is not useful if it is empty." + )] + fn panics_if_empty() { + Sut::new(IntentHash::sample(), IndexSet::new()); + } + + #[test] + #[should_panic( + expected = "entities_which_would_fail_auth must not contain duplicates." + )] + fn panics_if_duplicates() { + Sut::new( + IntentHash::sample(), + [ + AddressOfAccountOrPersona::sample(), + AddressOfAccountOrPersona::sample(), + ], + ); + } + + #[test] + fn new() { + let entities = [ + AddressOfAccountOrPersona::sample(), + AddressOfAccountOrPersona::sample_other(), + ]; + let sut = Sut::new(IntentHash::sample(), entities); + assert_eq!( + sut.entities_which_would_fail_auth(), + IndexSet::<_>::from_iter(entities.into_iter()) + ); + } +} diff --git a/crates/sargon/src/types/mod.rs b/crates/sargon/src/types/mod.rs index d6c267adc..81024eeda 100644 --- a/crates/sargon/src/types/mod.rs +++ b/crates/sargon/src/types/mod.rs @@ -1,7 +1,22 @@ +mod factor_sources_of_kind; mod ffi_url; +mod hd_signature; +mod hd_signature_input; +mod invalid_transaction_if_neglected; +mod owned_types; +mod samples; mod vector_image_type; mod vector_image_type_uniffi_fn; pub use ffi_url::*; pub use vector_image_type::*; pub use vector_image_type_uniffi_fn::*; + +pub(crate) use factor_sources_of_kind::*; +pub use hd_signature::*; +pub use hd_signature_input::*; +pub use invalid_transaction_if_neglected::*; +pub use owned_types::*; + +#[cfg(test)] +pub(crate) use samples::*; diff --git a/crates/sargon/src/types/owned_types/mod.rs b/crates/sargon/src/types/owned_types/mod.rs new file mode 100644 index 000000000..2f9d954bb --- /dev/null +++ b/crates/sargon/src/types/owned_types/mod.rs @@ -0,0 +1,5 @@ +mod owned; +mod owned_factor_instance; + +pub use owned::*; +pub use owned_factor_instance::*; diff --git a/crates/sargon/src/types/owned_types/owned.rs b/crates/sargon/src/types/owned_types/owned.rs new file mode 100644 index 000000000..a369475a0 --- /dev/null +++ b/crates/sargon/src/types/owned_types/owned.rs @@ -0,0 +1,26 @@ +use crate::prelude::*; + +/// Some value with a known owner - an account or persona. +#[derive(Clone, PartialEq, Eq, std::hash::Hash, derive_more::Debug)] +#[debug("{:#?}: {:#?}", owner, value)] +pub struct Owned { + /// The known owner - an account or persona - of `value`. + pub owner: AddressOfAccountOrPersona, + /// Some value known to be owned by `owner` - an account or persona. + pub value: T, +} + +impl Owned { + pub fn new(owner: AddressOfAccountOrPersona, value: T) -> Self { + Self { owner, value } + } +} + +impl HasSampleValues for Owned { + fn sample() -> Self { + Self::new(AddressOfAccountOrPersona::sample(), T::sample()) + } + fn sample_other() -> Self { + Self::new(AddressOfAccountOrPersona::sample_other(), T::sample_other()) + } +} diff --git a/crates/sargon/src/types/owned_types/owned_factor_instance.rs b/crates/sargon/src/types/owned_types/owned_factor_instance.rs new file mode 100644 index 000000000..9959422f1 --- /dev/null +++ b/crates/sargon/src/types/owned_types/owned_factor_instance.rs @@ -0,0 +1,41 @@ +use std::borrow::Borrow; + +use crate::prelude::*; + +/// A `HierarchicalDeterministicFactorInstance` with a known owner - an account or persona. +pub type OwnedFactorInstance = Owned; + +impl OwnedFactorInstance { + /// Constructs a new `OwnedFactorInstance`. + pub fn owned_factor_instance( + owner: AddressOfAccountOrPersona, + factor_instance: HierarchicalDeterministicFactorInstance, + ) -> Self { + Self::new(owner, factor_instance) + } + + /// The owned `HierarchicalDeterministicFactorInstance`, the value of this `OwnedFactorInstance`. + pub fn factor_instance(&self) -> &HierarchicalDeterministicFactorInstance { + &self.value + } + + pub fn factor_source_id(&self) -> FactorSourceIDFromHash { + self.factor_instance().factor_source_id + } + + /// Checks if this `OwnedFactorInstance` was created from the factor source + /// with id `factor_source_id`. + pub fn by_factor_source( + &self, + factor_source_id: impl Borrow, + ) -> bool { + let factor_source_id = factor_source_id.borrow(); + self.factor_source_id() == *factor_source_id + } +} + +impl From for HierarchicalDeterministicFactorInstance { + fn from(value: OwnedFactorInstance) -> Self { + value.value + } +} diff --git a/crates/sargon/src/types/samples/access_controller_address_samples.rs b/crates/sargon/src/types/samples/access_controller_address_samples.rs new file mode 100644 index 000000000..afef0f9b5 --- /dev/null +++ b/crates/sargon/src/types/samples/access_controller_address_samples.rs @@ -0,0 +1,31 @@ +use crate::{ + AccessControllerAddress, AccountAddress, IdentityAddress, IsNetworkAware, +}; + +impl AccessControllerAddress { + pub fn sample_from_account_address( + account_address: AccountAddress, + ) -> Self { + let node_id: [u8; 29] = account_address.node_id().as_bytes()[0..29] + .try_into() + .unwrap(); + + AccessControllerAddress::with_node_id_bytes( + &node_id, + account_address.network_id(), + ) + } + + pub fn sample_from_identity_address( + identity_address: IdentityAddress, + ) -> Self { + let node_id: [u8; 29] = identity_address.node_id().as_bytes()[0..29] + .try_into() + .unwrap(); + + AccessControllerAddress::with_node_id_bytes( + &node_id, + identity_address.network_id(), + ) + } +} diff --git a/crates/sargon/src/types/samples/account_address_samples.rs b/crates/sargon/src/types/samples/account_address_samples.rs new file mode 100644 index 000000000..2ba27edc6 --- /dev/null +++ b/crates/sargon/src/types/samples/account_address_samples.rs @@ -0,0 +1,11 @@ +use crate::prelude::*; + +impl AccountAddress { + pub fn sample_at(index: usize) -> Self { + Account::sample_at(index).address + } + + pub fn sample_all() -> Vec { + Account::sample_all().iter().map(|a| a.address).collect() + } +} diff --git a/crates/sargon/src/types/samples/account_samples.rs b/crates/sargon/src/types/samples/account_samples.rs new file mode 100644 index 000000000..c4cca2129 --- /dev/null +++ b/crates/sargon/src/types/samples/account_samples.rs @@ -0,0 +1,213 @@ +use crate::prelude::*; + +static ALL_ACCOUNT_SAMPLES: Lazy<[Account; 10]> = Lazy::new(|| { + [ + // Alice | 0 | Unsecurified { Device } + Account::sample_unsecurified_mainnet( + "Alice", + HierarchicalDeterministicFactorInstance::sample_fia0(), + ), + // Bob | 1 | Unsecurified { Ledger } + Account::sample_unsecurified_mainnet( + "Bob", + HierarchicalDeterministicFactorInstance::sample_fia1(), + ), + // Carla | 2 | Securified { Single Threshold only } + Account::sample_securified_mainnet( + "Carla", + AccountAddress::random(NetworkID::Mainnet), + || { + let idx = HDPathComponent::from(2); + GeneralRoleWithHierarchicalDeterministicFactorInstances::r2( + HierarchicalDeterministicFactorInstance::sample_id_to_instance( + CAP26EntityKind::Account, + idx, + ) + ) + }, + ), + // David | 3 | Securified { Single Override only } + Account::sample_securified_mainnet( + "David", + AccountAddress::random(NetworkID::Mainnet), + || { + let idx = HDPathComponent::from(3); + GeneralRoleWithHierarchicalDeterministicFactorInstances::r3( + HierarchicalDeterministicFactorInstance::sample_id_to_instance( + CAP26EntityKind::Account, + idx, + ) + ) + }, + ), + // Emily | 4 | Securified { Threshold factors only #3 } + Account::sample_securified_mainnet( + "Emily", + AccountAddress::random(NetworkID::Mainnet), + || { + let idx = HDPathComponent::from(4); + GeneralRoleWithHierarchicalDeterministicFactorInstances::r4( + HierarchicalDeterministicFactorInstance::sample_id_to_instance( + CAP26EntityKind::Account, + idx, + ) + ) + }, + ), + // Frank | 5 | Securified { Override factors only #2 } + Account::sample_securified_mainnet( + "Frank", + AccountAddress::random(NetworkID::Mainnet), + || { + let idx = HDPathComponent::from(5); + GeneralRoleWithHierarchicalDeterministicFactorInstances::r5( + HierarchicalDeterministicFactorInstance::sample_id_to_instance( + CAP26EntityKind::Account, + idx, + ) + ) + }, + ), + // Grace | 6 | Securified { Threshold #3 and Override factors #2 } + Account::sample_securified_mainnet( + "Grace", + AccountAddress::random(NetworkID::Mainnet), + || { + let idx = HDPathComponent::from(6); + GeneralRoleWithHierarchicalDeterministicFactorInstances::r6( + HierarchicalDeterministicFactorInstance::sample_id_to_instance( + CAP26EntityKind::Account, + idx, + ) + ) + }, + ), + // Ida | 7 | Securified { Threshold only # 5/5 } + Account::sample_securified_mainnet( + "Ida", + AccountAddress::random(NetworkID::Mainnet), + || { + let idx = HDPathComponent::from(7); + GeneralRoleWithHierarchicalDeterministicFactorInstances::r7( + HierarchicalDeterministicFactorInstance::sample_id_to_instance( + CAP26EntityKind::Account, + idx, + ) + ) + }, + ), + // Jenny | 8 | Unsecurified { Device } (fs10) + Account::sample_unsecurified_mainnet( + "Jenny", + HierarchicalDeterministicFactorInstance::sample_fia10(), + ), + // Klara | 9 | Securified { Threshold 1/1 and Override factors #1 } + Account::sample_securified_mainnet( + "Klara", + AccountAddress::random(NetworkID::Mainnet), + || { + let idx = HDPathComponent::from(9); + GeneralRoleWithHierarchicalDeterministicFactorInstances::r8( + HierarchicalDeterministicFactorInstance::sample_id_to_instance( + CAP26EntityKind::Account, + idx, + ) + ) + }, + ), + ] +}); + +impl Account { + pub fn sample_unsecurified_mainnet( + name: impl AsRef, + genesis_factor_instance: HierarchicalDeterministicFactorInstance, + ) -> Self { + Self { + network_id: NetworkID::Mainnet, + address: AccountAddress::new( + genesis_factor_instance.public_key.public_key, + NetworkID::Mainnet, + ), + display_name: DisplayName::new(name).unwrap(), + security_state: + UnsecuredEntityControl::with_transaction_signing_only( + genesis_factor_instance, + ) + .unwrap() + .into(), + appearance_id: Default::default(), + flags: Default::default(), + on_ledger_settings: Default::default(), + } + } + + pub fn sample_securified_mainnet( + name: impl AsRef, + address: AccountAddress, + make_role: impl Fn() -> GeneralRoleWithHierarchicalDeterministicFactorInstances, + ) -> Self { + let role = make_role(); + + let threshold_factors = role + .threshold_factors + .iter() + .map(|hd| hd.factor_instance()) + .collect::>(); + + let override_factors = role + .override_factors + .iter() + .map(|hd| hd.factor_instance()) + .collect::>(); + + let matrix = MatrixOfFactorInstances::new( + PrimaryRoleWithFactorInstances::new( + threshold_factors.clone(), + role.threshold, + override_factors.clone(), + ) + .unwrap(), + RecoveryRoleWithFactorInstances::new( + threshold_factors.clone(), + role.threshold, + override_factors.clone(), + ) + .unwrap(), + ConfirmationRoleWithFactorInstances::new( + threshold_factors.clone(), + role.threshold, + override_factors.clone(), + ) + .unwrap(), + ); + + Self { + network_id: NetworkID::Mainnet, + address, + display_name: DisplayName::new(name).unwrap(), + security_state: SecuredEntityControl { + access_controller_address: + AccessControllerAddress::sample_from_account_address( + address, + ), + security_structure: SecurityStructureOfFactorInstances { + security_structure_id: SecurityStructureID::sample(), + matrix_of_factors: matrix, + }, + } + .into(), + appearance_id: Default::default(), + flags: Default::default(), + on_ledger_settings: Default::default(), + } + } + + pub fn sample_at(index: usize) -> Self { + ALL_ACCOUNT_SAMPLES[index].clone() + } + + pub fn sample_all() -> Vec { + ALL_ACCOUNT_SAMPLES.to_vec() + } +} diff --git a/crates/sargon/src/types/samples/factor_source_ids_with_samples.rs b/crates/sargon/src/types/samples/factor_source_ids_with_samples.rs new file mode 100644 index 000000000..3ddc0e10b --- /dev/null +++ b/crates/sargon/src/types/samples/factor_source_ids_with_samples.rs @@ -0,0 +1,57 @@ +use crate::prelude::*; + +pub(crate) static ALL_FACTOR_SOURCE_ID_SAMPLES: Lazy< + [FactorSourceIDFromHash; 11], +> = Lazy::new(|| { + [ + FactorSourceIDFromHash::sample_device(), + FactorSourceIDFromHash::sample_ledger(), + FactorSourceIDFromHash::sample_ledger_other(), + FactorSourceIDFromHash::sample_arculus(), + FactorSourceIDFromHash::sample_arculus_other(), + FactorSourceIDFromHash::sample_passphrase(), + FactorSourceIDFromHash::sample_passphrase_other(), + FactorSourceIDFromHash::sample_off_device(), + FactorSourceIDFromHash::sample_off_device_other(), + FactorSourceIDFromHash::sample_security_questions(), + FactorSourceIDFromHash::sample_device_other(), + ] +}); + +impl FactorSourceIDFromHash { + pub fn sample_at(index: usize) -> Self { + ALL_FACTOR_SOURCE_ID_SAMPLES[index] + } + + pub fn sample_associated_mnemonic(&self) -> MnemonicWithPassphrase { + let id = *self; + if id == FactorSourceIDFromHash::sample_device() { + MnemonicWithPassphrase::sample_device() + } else if id == FactorSourceIDFromHash::sample_ledger() { + MnemonicWithPassphrase::sample_ledger() + } else if id == FactorSourceIDFromHash::sample_ledger_other() { + MnemonicWithPassphrase::sample_ledger_other() + } else if id == FactorSourceIDFromHash::sample_arculus() { + MnemonicWithPassphrase::sample_arculus() + } else if id == FactorSourceIDFromHash::sample_arculus_other() { + MnemonicWithPassphrase::sample_arculus_other() + } else if id == FactorSourceIDFromHash::sample_passphrase() { + MnemonicWithPassphrase::sample_passphrase() + } else if id == FactorSourceIDFromHash::sample_passphrase_other() { + MnemonicWithPassphrase::sample_passphrase_other() + } else if id == FactorSourceIDFromHash::sample_off_device() { + MnemonicWithPassphrase::sample_off_device() + } else if id == FactorSourceIDFromHash::sample_off_device_other() { + MnemonicWithPassphrase::sample_off_device_other() + } else if id == FactorSourceIDFromHash::sample_security_questions() { + MnemonicWithPassphrase::sample_security_questions() + } else if id == FactorSourceIDFromHash::sample_device_other() { + MnemonicWithPassphrase::sample_device_other() + } else { + panic!( + "Sample mnemonic with passphrase for id {} not found", + id.body.to_hex() + ) + } + } +} diff --git a/crates/sargon/src/types/samples/factor_source_samples.rs b/crates/sargon/src/types/samples/factor_source_samples.rs new file mode 100644 index 000000000..27d6f1745 --- /dev/null +++ b/crates/sargon/src/types/samples/factor_source_samples.rs @@ -0,0 +1,93 @@ +use crate::prelude::*; + +#[allow(dead_code)] +pub(crate) static ALL_FACTOR_SOURCE_SAMPLES: Lazy<[FactorSource; 11]> = + Lazy::new(|| { + crate::types::samples::ALL_FACTOR_SOURCE_ID_SAMPLES + .iter() + .map(FactorSource::sample_from) + .collect::>() + .try_into() + .unwrap() + }); + +#[allow(dead_code)] +impl FactorSource { + pub(crate) fn sample_at(index: usize) -> FactorSource { + ALL_FACTOR_SOURCE_SAMPLES[index].clone() + } + + pub(crate) fn sample_all() -> IndexSet { + IndexSet::from_iter(ALL_FACTOR_SOURCE_SAMPLES.clone()) + } + + fn sample_from(id: &FactorSourceIDFromHash) -> Self { + match id.kind { + FactorSourceKind::LedgerHQHardwareWallet => { + LedgerHardwareWalletFactorSource::new( + *id, + FactorSourceCommon::sample(), + LedgerHardwareWalletHint::new( + format!("Ledger @ {}", id.body.to_hex()).as_str(), + LedgerHardwareWalletModel::sample(), + ), + ) + .into() + } + FactorSourceKind::ArculusCard => ArculusCardFactorSource::new( + *id, + ArculusCardHint::new( + format!("Arculus @ {}", id.body.to_hex()).as_str(), + ArculusCardModel::ArculusColdStorageWallet, + ), + ) + .into(), + FactorSourceKind::Passphrase => { + PassphraseFactorSource::new(*id).into() + } + FactorSourceKind::SecurityQuestions => { + let sealed_mnemonic = SecurityQuestionsSealed_NOT_PRODUCTION_READY_Mnemonic::new_by_encrypting( + id.sample_associated_mnemonic().mnemonic, + Security_NOT_PRODUCTION_READY_QuestionsAndAnswers::sample(), + SecurityQuestions_NOT_PRODUCTION_READY_KDFScheme::default(), + EncryptionScheme::default(), + ).unwrap(); + + SecurityQuestions_NOT_PRODUCTION_READY_FactorSource::with_details( + *id, + FactorSourceCommon::sample(), + sealed_mnemonic + ).into() + } + FactorSourceKind::OffDeviceMnemonic => { + OffDeviceMnemonicFactorSource::new( + *id, + OffDeviceMnemonicHint::new( + DisplayName::new(format!( + "Off Device Mnemonic @ {}", + id.body.to_hex() + )) + .unwrap(), + ), + ) + .into() + } + FactorSourceKind::Device => DeviceFactorSource::new( + *id, + FactorSourceCommon::sample(), + DeviceFactorSourceHint::new( + format!("Device Name @ {}", id.body.to_hex()), + format!("Device Model @ {}", id.body.to_hex()), + None, + None, + None, + id.sample_associated_mnemonic().mnemonic.word_count, + ), + ) + .into(), + FactorSourceKind::TrustedContact => { + panic!("Trusted contact is not supported in sample tests") + } + } + } +} diff --git a/crates/sargon/src/types/samples/general_role_with_hd_factor_instance_samples.rs b/crates/sargon/src/types/samples/general_role_with_hd_factor_instance_samples.rs new file mode 100644 index 000000000..1e43d39e7 --- /dev/null +++ b/crates/sargon/src/types/samples/general_role_with_hd_factor_instance_samples.rs @@ -0,0 +1,98 @@ +use crate::prelude::*; + +impl GeneralRoleWithHierarchicalDeterministicFactorInstances { + /// Securified { Single Threshold only } + pub(crate) fn r2(fi: F) -> Self + where + F: Fn( + FactorSourceIDFromHash, + ) -> HierarchicalDeterministicFactorInstance, + { + Self::single_threshold(fi(FactorSourceIDFromHash::sample_at(0))) + } + + /// Securified { Single Override only } + pub(crate) fn r3(fi: F) -> Self + where + F: Fn( + FactorSourceIDFromHash, + ) -> HierarchicalDeterministicFactorInstance, + { + Self::single_override(fi(FactorSourceIDFromHash::sample_at(1))) + } + + /// Securified { Threshold factors only #3 } + pub(crate) fn r4(fi: F) -> Self + where + F: Fn( + FactorSourceIDFromHash, + ) -> HierarchicalDeterministicFactorInstance, + { + type F = FactorSourceIDFromHash; + Self::threshold_only( + [F::sample_at(0), F::sample_at(3), F::sample_at(5)].map(fi), + 2, + ) + .unwrap() + } + + /// Securified { Override factors only #2 } + pub(crate) fn r5(fi: F) -> Self + where + F: Fn( + FactorSourceIDFromHash, + ) -> HierarchicalDeterministicFactorInstance, + { + type F = FactorSourceIDFromHash; + Self::override_only([F::sample_at(1), F::sample_at(4)].map(&fi)) + } + + /// Securified { Threshold #3 and Override factors #2 } + pub(crate) fn r6(fi: F) -> Self + where + F: Fn( + FactorSourceIDFromHash, + ) -> HierarchicalDeterministicFactorInstance, + { + type F = FactorSourceIDFromHash; + Self::new( + [F::sample_at(0), F::sample_at(3), F::sample_at(5)].map(&fi), + 2, + [F::sample_at(1), F::sample_at(4)].map(&fi), + ) + .unwrap() + } + + /// Securified { Threshold only # 5/5 } + pub(crate) fn r7(fi: F) -> Self + where + F: Fn( + FactorSourceIDFromHash, + ) -> HierarchicalDeterministicFactorInstance, + { + type F = FactorSourceIDFromHash; + Self::threshold_only( + [ + F::sample_at(2), + F::sample_at(6), + F::sample_at(7), + F::sample_at(8), + F::sample_at(9), + ] + .map(&fi), + 5, + ) + .unwrap() + } + /// Securified { Threshold 1/1 and Override factors #1 } + pub(crate) fn r8(fi: F) -> Self + where + F: Fn( + FactorSourceIDFromHash, + ) -> HierarchicalDeterministicFactorInstance, + { + type F = FactorSourceIDFromHash; + Self::new([F::sample_at(1)].map(&fi), 1, [F::sample_at(8)].map(&fi)) + .unwrap() + } +} diff --git a/crates/sargon/src/types/samples/hierarchical_deterministic_factor_instance_samples.rs b/crates/sargon/src/types/samples/hierarchical_deterministic_factor_instance_samples.rs new file mode 100644 index 000000000..f8b331236 --- /dev/null +++ b/crates/sargon/src/types/samples/hierarchical_deterministic_factor_instance_samples.rs @@ -0,0 +1,81 @@ +use crate::prelude::*; + +impl HierarchicalDeterministicFactorInstance { + pub(crate) fn sample_id_to_instance( + entity_kind: CAP26EntityKind, + index: HDPathComponent, + ) -> impl Fn(FactorSourceIDFromHash) -> Self { + move |id: FactorSourceIDFromHash| { + Self::new_for_entity(id, entity_kind, index) + } + } + + pub fn sample_mainnet_tx_account( + index: HDPathComponent, + factor_source_id: FactorSourceIDFromHash, + ) -> Self { + Self::new_for_entity(factor_source_id, CAP26EntityKind::Account, index) + } + + pub fn sample_mainnet_tx_identity( + index: HDPathComponent, + factor_source_id: FactorSourceIDFromHash, + ) -> Self { + Self::new_for_entity(factor_source_id, CAP26EntityKind::Identity, index) + } + + /// 0 | unsecurified | device + pub fn sample_fi0(entity_kind: CAP26EntityKind) -> Self { + Self::new_for_entity( + FactorSourceIDFromHash::sample_at(0), + entity_kind, + HDPathComponent::from(0), + ) + } + + /// Account: 0 | unsecurified | device + pub fn sample_fia0() -> Self { + Self::sample_fi0(CAP26EntityKind::Account) + } + /// Identity: 0 | unsecurified | device + pub fn sample_fii0() -> Self { + Self::sample_fi0(CAP26EntityKind::Identity) + } + + /// 1 | unsecurified | ledger + pub fn sample_fi1(entity_kind: CAP26EntityKind) -> Self { + Self::new_for_entity( + FactorSourceIDFromHash::sample_at(1), + entity_kind, + HDPathComponent::from(1), + ) + } + + /// Account: 1 | unsecurified | ledger + pub fn sample_fia1() -> Self { + Self::sample_fi1(CAP26EntityKind::Account) + } + /// Identity: 1 | unsecurified | ledger + pub fn sample_fii1() -> Self { + Self::sample_fi1(CAP26EntityKind::Identity) + } + + /// 8 | Unsecurified { Device } (fs10) + pub fn sample_fi10(entity_kind: CAP26EntityKind) -> Self { + Self::new_for_entity( + FactorSourceIDFromHash::sample_at(10), + entity_kind, + HDPathComponent::from(8), + ) + } + + /// Account: 8 | Unsecurified { Device } (fs10) + pub fn sample_fia10() -> Self { + Self::sample_fi10(CAP26EntityKind::Account) + } + + /// Identity: 8 | Unsecurified { Device } (fs10) + pub fn sample_fii10() -> Self { + Self::sample_fi10(CAP26EntityKind::Identity) + } +} diff --git a/crates/sargon/src/types/samples/identity_address_samples.rs b/crates/sargon/src/types/samples/identity_address_samples.rs new file mode 100644 index 000000000..4a788ecf7 --- /dev/null +++ b/crates/sargon/src/types/samples/identity_address_samples.rs @@ -0,0 +1,11 @@ +use crate::prelude::*; + +impl IdentityAddress { + pub fn sample_at(index: usize) -> Self { + Persona::sample_at(index).address + } + + pub fn sample_all() -> Vec { + Persona::sample_all().iter().map(|a| a.address).collect() + } +} diff --git a/crates/sargon/src/types/samples/mod.rs b/crates/sargon/src/types/samples/mod.rs new file mode 100644 index 000000000..9a15a8208 --- /dev/null +++ b/crates/sargon/src/types/samples/mod.rs @@ -0,0 +1,23 @@ +mod access_controller_address_samples; +mod account_address_samples; +mod account_samples; +mod factor_source_ids_with_samples; +mod factor_source_samples; +mod general_role_with_hd_factor_instance_samples; +mod hierarchical_deterministic_factor_instance_samples; +mod identity_address_samples; +mod persona_samples; +mod profile_samples; +mod transaction_intent_samples; + +pub use access_controller_address_samples::*; +pub use account_address_samples::*; +pub use account_samples::*; +pub use factor_source_ids_with_samples::*; +pub use factor_source_samples::*; +pub use general_role_with_hd_factor_instance_samples::*; +pub use hierarchical_deterministic_factor_instance_samples::*; +pub use identity_address_samples::*; +pub use persona_samples::*; +pub use profile_samples::*; +pub use transaction_intent_samples::*; diff --git a/crates/sargon/src/types/samples/persona_samples.rs b/crates/sargon/src/types/samples/persona_samples.rs new file mode 100644 index 000000000..e7d548bbc --- /dev/null +++ b/crates/sargon/src/types/samples/persona_samples.rs @@ -0,0 +1,180 @@ +use crate::prelude::*; + +static ALL_PERSONA_SAMPLES: Lazy<[Persona; 8]> = Lazy::new(|| { + [ + // Satoshi | 0 | Unsecurified { Device } + Persona::sample_unsecurified_mainnet( + "Satoshi", + HierarchicalDeterministicFactorInstance::sample_fii0(), + ), + // Batman | 1 | Unsecurified { Ledger } + Persona::sample_unsecurified_mainnet( + "Batman", + HierarchicalDeterministicFactorInstance::sample_fii1(), + ), + // Ziggy | 2 | Securified { Single Threshold only } + Persona::sample_securified_mainnet( + "Ziggy", + IdentityAddress::random(NetworkID::Mainnet), + || { + let idx = HDPathComponent::from(2); + GeneralRoleWithHierarchicalDeterministicFactorInstances::r2(HierarchicalDeterministicFactorInstance::sample_id_to_instance( + CAP26EntityKind::Identity, + idx, + )) + }, + ), + // Superman | 3 | Securified { Single Override only } + Persona::sample_securified_mainnet( + "Superman", + IdentityAddress::random(NetworkID::Mainnet), + || { + let idx = HDPathComponent::from(3); + GeneralRoleWithHierarchicalDeterministicFactorInstances::r3(HierarchicalDeterministicFactorInstance::sample_id_to_instance( + CAP26EntityKind::Identity, + idx, + )) + }, + ), + // Banksy | 4 | Securified { Threshold factors only #3 } + Persona::sample_securified_mainnet( + "Banksy", + IdentityAddress::random(NetworkID::Mainnet), + || { + let idx = HDPathComponent::from(4); + GeneralRoleWithHierarchicalDeterministicFactorInstances::r4(HierarchicalDeterministicFactorInstance::sample_id_to_instance( + CAP26EntityKind::Identity, + idx, + )) + }, + ), + // Voltaire | 5 | Securified { Override factors only #2 } + Persona::sample_securified_mainnet( + "Voltaire", + IdentityAddress::random(NetworkID::Mainnet), + || { + let idx = HDPathComponent::from(5); + GeneralRoleWithHierarchicalDeterministicFactorInstances::r5(HierarchicalDeterministicFactorInstance::sample_id_to_instance( + CAP26EntityKind::Identity, + idx, + )) + }, + ), + // Kasparov | 6 | Securified { Threshold #3 and Override factors #2 } + Persona::sample_securified_mainnet( + "Kasparov", + IdentityAddress::random(NetworkID::Mainnet), + || { + let idx = HDPathComponent::from(6); + GeneralRoleWithHierarchicalDeterministicFactorInstances::r6(HierarchicalDeterministicFactorInstance::sample_id_to_instance( + CAP26EntityKind::Identity, + idx, + )) + }, + ), + // Pelé | 7 | Securified { Threshold only # 5/5 } + Persona::sample_securified_mainnet( + "Pelé", + IdentityAddress::random(NetworkID::Mainnet), + || { + let idx = HDPathComponent::from(7); + GeneralRoleWithHierarchicalDeterministicFactorInstances::r7(HierarchicalDeterministicFactorInstance::sample_id_to_instance( + CAP26EntityKind::Identity, + idx, + )) + }, + ), + ] +}); + +impl Persona { + pub fn sample_unsecurified_mainnet( + name: impl AsRef, + genesis_factor_instance: HierarchicalDeterministicFactorInstance, + ) -> Self { + Self { + network_id: NetworkID::Mainnet, + address: IdentityAddress::new( + genesis_factor_instance.public_key.public_key, + NetworkID::Mainnet, + ), + display_name: DisplayName::new(name).unwrap(), + security_state: + UnsecuredEntityControl::with_transaction_signing_only( + genesis_factor_instance, + ) + .unwrap() + .into(), + flags: Default::default(), + persona_data: Default::default(), + } + } + + pub fn sample_securified_mainnet( + name: impl AsRef, + address: IdentityAddress, + make_role: impl Fn() -> GeneralRoleWithHierarchicalDeterministicFactorInstances, + ) -> Self { + let role = make_role(); + + let threshold_factors = role + .threshold_factors + .iter() + .map(|hd| hd.factor_instance()) + .collect::>(); + + let override_factors = role + .override_factors + .iter() + .map(|hd| hd.factor_instance()) + .collect::>(); + + let matrix = MatrixOfFactorInstances::new( + PrimaryRoleWithFactorInstances::new( + threshold_factors.clone(), + role.threshold, + override_factors.clone(), + ) + .unwrap(), + RecoveryRoleWithFactorInstances::new( + threshold_factors.clone(), + role.threshold, + override_factors.clone(), + ) + .unwrap(), + ConfirmationRoleWithFactorInstances::new( + threshold_factors.clone(), + role.threshold, + override_factors.clone(), + ) + .unwrap(), + ); + + Self { + network_id: NetworkID::Mainnet, + address, + display_name: DisplayName::new(name).unwrap(), + security_state: SecuredEntityControl { + access_controller_address: + AccessControllerAddress::sample_from_identity_address( + address, + ), + security_structure: SecurityStructureOfFactorInstances { + security_structure_id: SecurityStructureID::sample(), + matrix_of_factors: matrix, + }, + } + .into(), + flags: Default::default(), + persona_data: Default::default(), + } + } + + pub fn sample_at(index: usize) -> Self { + ALL_PERSONA_SAMPLES[index].clone() + } + + pub fn sample_all() -> Vec { + ALL_PERSONA_SAMPLES.to_vec() + } +} diff --git a/crates/sargon/src/types/samples/profile_samples.rs b/crates/sargon/src/types/samples/profile_samples.rs new file mode 100644 index 000000000..c9a906097 --- /dev/null +++ b/crates/sargon/src/types/samples/profile_samples.rs @@ -0,0 +1,52 @@ +use crate::prelude::*; + +impl Profile { + pub fn sample_from<'a, 'p>( + factor_sources: impl IntoIterator, + accounts: impl IntoIterator, + personas: impl IntoIterator, + ) -> Self { + let mut networks = ProfileNetworks::new(); + + accounts.into_iter().for_each(|a| { + if networks.contains_id(a.network_id) { + networks.update_with(a.network_id, |n| { + n.accounts.append(a.clone()); + }); + } else { + let network = ProfileNetwork::new( + a.network_id, + Accounts::just(a.clone()), + Personas::new(), + AuthorizedDapps::default(), + ResourcePreferences::default(), + ); + networks.append(network); + } + }); + + personas.into_iter().for_each(|p| { + if networks.contains_id(p.network_id) { + networks.update_with(p.network_id, |n| { + n.personas.append(p.clone()); + }); + } else { + let network = ProfileNetwork::new( + p.network_id, + Accounts::new(), + Personas::just(p.clone()), + AuthorizedDapps::default(), + ResourcePreferences::default(), + ); + networks.append(network); + } + }); + + Profile { + header: Header::sample(), + factor_sources: FactorSources::from_iter(factor_sources), + app_preferences: Default::default(), + networks, + } + } +} diff --git a/crates/sargon/src/types/samples/transaction_intent_samples.rs b/crates/sargon/src/types/samples/transaction_intent_samples.rs new file mode 100644 index 000000000..9c466ee31 --- /dev/null +++ b/crates/sargon/src/types/samples/transaction_intent_samples.rs @@ -0,0 +1,116 @@ +use crate::prelude::*; +use radix_engine_toolkit::models::canonical_address_types::NetworkId; +use reqwest::Identity; + +impl TransactionIntent { + /// Returns a sample intent that its transaction summary will involve all the + /// `accounts_requiring_auth` and `personas_requiring_auth` in entities requiring auth. + /// This can be accomplished by building a manifest that constructs owner keys from these + /// entities + pub fn entities_requiring_auth<'a, 'p>( + accounts_requiring_auth: impl IntoIterator, + personas_requiring_auth: impl IntoIterator, + ) -> Self { + Self::new_requiring_auth( + accounts_requiring_auth.into_iter().map(|a| a.address), + personas_requiring_auth.into_iter().map(|p| p.address), + ) + } + + /// Returns a sample intent that its transaction summary will involve all the + /// `account_addresses_requiring_auth` and `identity_addresses_requiring_auth` in + /// entities requiring auth. + /// This can be accomplished by building a manifest that constructs owner keys from these + /// entity addresses. + pub fn new_requiring_auth( + account_addresses_requiring_auth: impl IntoIterator, + identity_addresses_requiring_auth: impl IntoIterator, + ) -> Self { + let mut network_id: Option = None; + + let all_addresses = account_addresses_requiring_auth + .into_iter() + .map(AddressOfAccountOrPersona::from) + .chain( + identity_addresses_requiring_auth + .into_iter() + .map(AddressOfAccountOrPersona::Identity), + ) + .collect::>(); + + all_addresses.iter().for_each(|address| { + if let Some(network_id) = network_id { + assert_eq!(network_id, address.network_id()) + } else { + network_id = Some(address.network_id()) + } + }); + + let metadata = + HashMap::>::from_iter( + all_addresses.into_iter().map(|address| { + let pub_key = Ed25519PrivateKey::generate().public_key(); + ( + address, + vec![PublicKeyHash::hash(PublicKey::Ed25519(pub_key))], + ) + }), + ); + + let mut builder = ScryptoManifestBuilder::new(); + let network_id = network_id.unwrap_or_default(); + + for (address, public_key_hashes) in metadata { + builder = builder.set_metadata( + address.scrypto(), + MetadataKey::OwnerKeys, + ScryptoMetadataValue::PublicKeyHashArray( + public_key_hashes + .into_iter() + .map(|h| h.into()) + .collect_vec(), + ), + ); + } + + let manifest = TransactionManifest::sargon_built(builder, network_id); + + Self::new(TransactionHeader::sample(), manifest, Message::None).unwrap() + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn account_addresses_and_identity_addresses_require_auth() { + let accounts = AccountAddress::sample_all(); + let identities = IdentityAddress::sample_all(); + + let intent = TransactionIntent::new_requiring_auth( + accounts.clone(), + identities.clone(), + ); + + let summary = intent.manifest_summary(); + + assert_eq!( + accounts.iter().sorted().collect_vec(), + summary + .addresses_of_accounts_requiring_auth + .iter() + .sorted() + .collect_vec() + ); + + assert_eq!( + identities.iter().sorted().collect_vec(), + summary + .addresses_of_personas_requiring_auth + .iter() + .sorted() + .collect_vec() + ); + } +} diff --git a/crates/sargon/src/wrapped_radix_engine_toolkit/low_level/transaction_intent.rs b/crates/sargon/src/wrapped_radix_engine_toolkit/low_level/transaction_intent.rs index c1e38a702..fb141a699 100644 --- a/crates/sargon/src/wrapped_radix_engine_toolkit/low_level/transaction_intent.rs +++ b/crates/sargon/src/wrapped_radix_engine_toolkit/low_level/transaction_intent.rs @@ -46,6 +46,10 @@ impl TransactionIntent { ) } + pub fn manifest_summary(&self) -> ManifestSummary { + self.manifest.summary() + } + pub fn compile(&self) -> BagOfBytes { compile_intent(ScryptoIntent::from(self.clone())) .expect("Should always be able to compile an Intent") diff --git a/crates/sargon/src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/execution_summary/transaction_manifest_execution_summary.rs b/crates/sargon/src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/execution_summary/transaction_manifest_execution_summary.rs index 44316fa16..c3130452e 100644 --- a/crates/sargon/src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/execution_summary/transaction_manifest_execution_summary.rs +++ b/crates/sargon/src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/execution_summary/transaction_manifest_execution_summary.rs @@ -14,7 +14,7 @@ impl TransactionManifest { let network_definition = self.network_id().network_definition(); let receipt = serde_json::from_str::< ScryptoSerializableToolkitTransactionReceipt, - >(&engine_toolkit_receipt.as_ref()) + >(engine_toolkit_receipt.as_ref()) .ok() .and_then(|receipt| { receipt diff --git a/crates/sargon/tests/integration/main.rs b/crates/sargon/tests/integration/main.rs index 39c6f3c92..82f9d2ea0 100644 --- a/crates/sargon/tests/integration/main.rs +++ b/crates/sargon/tests/integration/main.rs @@ -1,6 +1,5 @@ #[cfg(test)] mod integration_tests { - use std::time::Duration; use actix_rt::time::timeout; @@ -103,11 +102,11 @@ mod integration_tests { let sut = gateway_client.dry_run_transaction( intent, vec![ - Ed25519PublicKey::from_hex( - "48d24f09b43d50f3acd58cf8509a57c8f306d94b945bd9b7e6ebcf6691eed3b6".to_owned() - ).unwrap().into() - ] - ); + Ed25519PublicKey::from_hex( + "48d24f09b43d50f3acd58cf8509a57c8f306d94b945bd9b7e6ebcf6691eed3b6".to_owned() + ).unwrap().into() + ], + ); // ACT let engine_toolkit_receipt = timeout(MAX, sut).await.unwrap().unwrap(); @@ -219,7 +218,7 @@ mod integration_tests { let gumball_address = AccountAddress::try_from_bech32( "account_tdx_2_129nx5lgkk3fz9gqf3clppeljkezeyyymqqejzp97tpk0r8els7hg3j", ) - .unwrap(); + .unwrap(); let gateway_client = new_gateway_client(NetworkID::Stokenet); let sut = gateway_client.fetch_dapp_metadata(gumball_address); @@ -231,7 +230,7 @@ mod integration_tests { Url::parse( "https://stokenet-gumball-club.radixdlt.com/assets/gumball-club.png" ) - .unwrap() + .unwrap() ) ); } @@ -258,4 +257,214 @@ mod integration_tests { .unwrap(); assert_eq!(status, TransactionStatusResponsePayloadStatus::Pending); } + + mod signing { + use super::*; + use radix_common::prelude::indexmap::IndexSet; + use std::sync::Arc; + + struct TestLazySignMinimumInteractors; + struct TestLazySignMinimumInteractor; + + #[async_trait::async_trait] + impl PolyFactorSignInteractor for TestLazySignMinimumInteractor { + async fn sign( + &self, + request: PolyFactorSignRequest, + ) -> SignWithFactorsOutcome { + let mut signatures = IndexSet::::new(); + for (_, req) in request.per_factor_source.iter() { + let resp = ::sign( + self, + MonoFactorSignRequest::new( + req.clone(), + request.invalid_transactions_if_neglected.clone(), + ), + ) + .await; + + match resp { + SignWithFactorsOutcome::Signed { + produced_signatures, + } => { + signatures.extend( + produced_signatures + .signatures + .into_iter() + .flat_map(|(_, xs)| xs) + .collect::>(), + ); + } + SignWithFactorsOutcome::Neglected(_) => { + return SignWithFactorsOutcome::Neglected( + NeglectedFactors::new( + NeglectFactorReason::UserExplicitlySkipped, + request.factor_source_ids(), + ), + ); + } + } + } + SignWithFactorsOutcome::signed(SignResponse::with_signatures( + signatures, + )) + } + } + + #[async_trait::async_trait] + impl MonoFactorSignInteractor for TestLazySignMinimumInteractor { + async fn sign( + &self, + request: MonoFactorSignRequest, + ) -> SignWithFactorsOutcome { + if request.invalid_transactions_if_neglected.is_empty() { + return SignWithFactorsOutcome::Neglected( + NeglectedFactors::new( + NeglectFactorReason::UserExplicitlySkipped, + IndexSet::just(request.input.factor_source_id), + ), + ); + } + let signatures = request + .input + .per_transaction + .into_iter() + .flat_map(|r| { + r.signature_inputs() + .iter() + .map(|x| HDSignature::fake_sign_by_looking_up_mnemonic_amongst_samples(x.clone())) + .collect::>() + }) + .collect::>(); + SignWithFactorsOutcome::Signed { + produced_signatures: SignResponse::with_signatures( + signatures, + ), + } + } + } + + impl SignInteractors for TestLazySignMinimumInteractors { + fn interactor_for(&self, kind: FactorSourceKind) -> SignInteractor { + match kind { + FactorSourceKind::Device => SignInteractor::mono(Arc::new( + TestLazySignMinimumInteractor, + )), + _ => SignInteractor::poly(Arc::new( + TestLazySignMinimumInteractor, + )), + } + } + } + + #[actix_rt::test] + async fn valid() { + type FI = HierarchicalDeterministicFactorInstance; + + let f0 = FactorSource::sample_ledger(); + let f1 = FactorSource::sample_device_babylon(); + let f2 = FactorSource::sample_device_babylon_other(); + let f3 = FactorSource::sample_arculus(); + let f4 = FactorSource::sample_off_device(); + + let alice = Account::sample_securified_mainnet( + "Alice", + AccountAddress::sample_at(0), + || { + let i = HDPathComponent::from(0); + GeneralRoleWithHierarchicalDeterministicFactorInstances::threshold_only( + [ + FI::sample_mainnet_tx_account(i, *f0.factor_source_id().as_hash().unwrap()), // SKIPPED + FI::sample_mainnet_tx_account(i, *f1.factor_source_id().as_hash().unwrap()), + FI::sample_mainnet_tx_account(i, *f2.factor_source_id().as_hash().unwrap()), + ], + 2, + ).unwrap() + }, + ); + + let bob = Account::sample_securified_mainnet( + "Bob", + AccountAddress::sample_at(1), + || { + let i = HDPathComponent::from(1); + GeneralRoleWithHierarchicalDeterministicFactorInstances::override_only([ + FI::sample_mainnet_tx_account( + i, + *f3.factor_source_id().as_hash().unwrap(), + ) + ]) + }, + ); + + let carol = Account::sample_securified_mainnet( + "Carol", + AccountAddress::sample_at(2), + || { + let i = HDPathComponent::from(2); + GeneralRoleWithHierarchicalDeterministicFactorInstances::new( + [FI::sample_mainnet_tx_account( + i, + *f2.factor_source_id().as_hash().unwrap(), + )], + 1, + [FI::sample_mainnet_tx_account( + i, + *f4.factor_source_id().as_hash().unwrap(), + )], + ).unwrap() + }, + ); + + let satoshi = Persona::sample_unsecurified_mainnet( + "Satoshi", + HierarchicalDeterministicFactorInstance::sample_mainnet_tx_identity( + HDPathComponent::from(0), + *f4.factor_source_id().as_hash().unwrap(), + ), + ); + + let tx0 = + TransactionIntent::new_requiring_auth([alice.address], []); + let tx1 = TransactionIntent::new_requiring_auth( + [alice.address, bob.address, carol.address], + [satoshi.address], + ); + let tx2 = TransactionIntent::new_requiring_auth( + [bob.address], + [satoshi.address], + ); + + let transactions = [tx0, tx1, tx2]; + + let profile = Profile::sample_from( + [f0.clone(), f1, f2, f3, f4], + [&alice, &bob, &carol], + [&satoshi], + ); + + let collector = SignaturesCollector::new( + SigningFinishEarlyStrategy::default(), + transactions, + Arc::new(TestLazySignMinimumInteractors), + &profile, + RoleKind::Primary, + ) + .unwrap(); + + let outcome = collector.collect_signatures().await; + + assert!(outcome.successful()); + assert_eq!( + outcome.signatures_of_successful_transactions().len(), + 10 + ); + assert_eq!( + outcome.ids_of_neglected_factor_sources(), + IndexSet::::just( + *f0.factor_source_id().as_hash().unwrap() + ) + ); + } + } } diff --git a/jvm/sargon-android/src/androidTest/kotlin/com/radixdlt/sargon/os/driver/AndroidStorageDriverTest.kt b/jvm/sargon-android/src/androidTest/kotlin/com/radixdlt/sargon/os/driver/AndroidStorageDriverTest.kt index fc44800c7..318b5f50c 100644 --- a/jvm/sargon-android/src/androidTest/kotlin/com/radixdlt/sargon/os/driver/AndroidStorageDriverTest.kt +++ b/jvm/sargon-android/src/androidTest/kotlin/com/radixdlt/sargon/os/driver/AndroidStorageDriverTest.kt @@ -8,6 +8,7 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.MediumTest import com.radixdlt.sargon.Profile +import com.radixdlt.sargon.ProfileId import com.radixdlt.sargon.SecureStorageKey import com.radixdlt.sargon.UnsafeStorageKey import com.radixdlt.sargon.Uuid @@ -51,7 +52,7 @@ class AndroidStorageDriverTest { val sut = sut(testContext, backgroundScope) val profile = Profile.sample() val jsonBytes = bagOfBytes(profile.toJson()) - val key = SecureStorageKey.ProfileSnapshot + val key = SecureStorageKey.ProfileSnapshot(profileId = ProfileId.randomUUID()) sut.saveData(key, jsonBytes) val receivedBytes = sut.loadData(key) @@ -67,7 +68,7 @@ class AndroidStorageDriverTest { val sut = sut(testContext, backgroundScope) val profile = Profile.sample() val jsonBytes = bagOfBytes(profile.toJson()) - val key = SecureStorageKey.ProfileSnapshot + val key = SecureStorageKey.ProfileSnapshot(profileId = ProfileId.randomUUID()) sut.saveData(key, jsonBytes) assertEquals( profile, diff --git a/jvm/sargon-android/src/main/java/com/radixdlt/sargon/extensions/FactorSource.kt b/jvm/sargon-android/src/main/java/com/radixdlt/sargon/extensions/FactorSource.kt index 03906418c..7c277fce7 100644 --- a/jvm/sargon-android/src/main/java/com/radixdlt/sargon/extensions/FactorSource.kt +++ b/jvm/sargon-android/src/main/java/com/radixdlt/sargon/extensions/FactorSource.kt @@ -9,6 +9,7 @@ import com.radixdlt.sargon.HostInfo import com.radixdlt.sargon.LedgerHardwareWalletFactorSource import com.radixdlt.sargon.MnemonicWithPassphrase import com.radixdlt.sargon.OffDeviceMnemonicFactorSource +import com.radixdlt.sargon.PassphraseFactorSource import com.radixdlt.sargon.SecurityQuestionsNotProductionReadyFactorSource import com.radixdlt.sargon.TrustedContactFactorSource import com.radixdlt.sargon.deviceFactorSourceIsMainBdfs @@ -25,6 +26,7 @@ val FactorSource.id: FactorSourceId is FactorSource.OffDeviceMnemonic -> value.id.asGeneral() is FactorSource.SecurityQuestions -> value.id.asGeneral() is FactorSource.TrustedContact -> value.id.asGeneral() + is FactorSource.Passphrase -> value.id.asGeneral() } val FactorSource.kind: FactorSourceKind @@ -35,6 +37,7 @@ val FactorSource.kind: FactorSourceKind is FactorSource.OffDeviceMnemonic -> value.kind is FactorSource.SecurityQuestions -> value.kind is FactorSource.TrustedContact -> value.kind + is FactorSource.Passphrase -> value.kind } fun DeviceFactorSource.asGeneral() = FactorSource.Device(value = this) @@ -90,3 +93,6 @@ val SecurityQuestionsNotProductionReadyFactorSource.kind: FactorSourceKind val TrustedContactFactorSource.kind: FactorSourceKind get() = FactorSourceKind.TRUSTED_CONTACT +val PassphraseFactorSource.kind: FactorSourceKind + get() = FactorSourceKind.PASSPHRASE +