From 8c80c839f560efca8d143709a8d47dcd1a2e9f05 Mon Sep 17 00:00:00 2001 From: Shunkichi Sato <49983831+s8sato@users.noreply.github.com> Date: Mon, 18 Nov 2024 17:08:41 +0900 Subject: [PATCH] refactor(multisig)!: move from triggers to custom instructions (#5217) BREAKING CHANGES: - (api-changes) `MultisigRegister` `MultisigPropose` `MultisigApprove` custom instructions Signed-off-by: Shunkichi Sato <49983831+s8sato@users.noreply.github.com> --- Cargo.lock | 14 +- Cargo.toml | 2 - crates/iroha/Cargo.toml | 3 +- crates/iroha/src/lib.rs | 2 +- crates/iroha/tests/multisig.rs | 441 ++++++++---------- crates/iroha_cli/src/main.rs | 122 ++--- crates/iroha_data_model/src/visit.rs | 6 +- crates/iroha_executor/src/default/isi/mod.rs | 49 ++ .../src/default/isi/multisig/account.rs | 64 +++ .../src/default/isi/multisig/mod.rs | 52 +++ .../src/default/isi/multisig/transaction.rs | 249 ++++++++++ .../src/{default.rs => default/mod.rs} | 155 ++---- crates/iroha_executor/src/lib.rs | 4 + crates/iroha_executor/src/permission.rs | 34 +- crates/iroha_executor_data_model/Cargo.toml | 1 + crates/iroha_executor_data_model/src/isi.rs | 118 +++++ crates/iroha_executor_data_model/src/lib.rs | 1 + .../src/permission.rs | 10 - crates/iroha_executor_derive/src/default.rs | 6 +- crates/iroha_genesis/src/lib.rs | 49 -- crates/iroha_kagami/src/genesis/generate.rs | 24 +- crates/iroha_schema/src/lib.rs | 4 +- crates/iroha_schema_gen/Cargo.toml | 1 - crates/iroha_schema_gen/src/lib.rs | 27 +- crates/iroha_test_network/src/lib.rs | 2 +- .../libs/iroha_multisig_data_model/Cargo.toml | 19 - .../libs/iroha_multisig_data_model/src/lib.rs | 74 --- defaults/genesis.json | 75 +-- docs/source/references/schema.json | 62 ++- scripts/build_wasm.sh | 3 - scripts/tests/instructions.json | 2 +- scripts/tests/multisig.recursion.sh | 13 +- scripts/tests/multisig.sh | 4 +- wasm/Cargo.toml | 1 - wasm/libs/multisig_accounts/Cargo.toml | 22 - wasm/libs/multisig_accounts/src/lib.rs | 150 ------ wasm/libs/multisig_domains/Cargo.toml | 21 - wasm/libs/multisig_domains/src/lib.rs | 77 --- wasm/libs/multisig_transactions/Cargo.toml | 21 - wasm/libs/multisig_transactions/src/lib.rs | 228 --------- .../src/lib.rs | 4 +- .../src/lib.rs | 4 +- 42 files changed, 915 insertions(+), 1305 deletions(-) create mode 100644 crates/iroha_executor/src/default/isi/mod.rs create mode 100644 crates/iroha_executor/src/default/isi/multisig/account.rs create mode 100644 crates/iroha_executor/src/default/isi/multisig/mod.rs create mode 100644 crates/iroha_executor/src/default/isi/multisig/transaction.rs rename crates/iroha_executor/src/{default.rs => default/mod.rs} (93%) create mode 100644 crates/iroha_executor_data_model/src/isi.rs delete mode 100644 data_model/libs/iroha_multisig_data_model/Cargo.toml delete mode 100644 data_model/libs/iroha_multisig_data_model/src/lib.rs delete mode 100644 wasm/libs/multisig_accounts/Cargo.toml delete mode 100644 wasm/libs/multisig_accounts/src/lib.rs delete mode 100644 wasm/libs/multisig_domains/Cargo.toml delete mode 100644 wasm/libs/multisig_domains/src/lib.rs delete mode 100644 wasm/libs/multisig_transactions/Cargo.toml delete mode 100644 wasm/libs/multisig_transactions/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 4940c0f920d..2f5fc18928a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2919,7 +2919,6 @@ dependencies = [ "iroha_executor_data_model", "iroha_genesis", "iroha_logger", - "iroha_multisig_data_model", "iroha_primitives", "iroha_telemetry", "iroha_test_network", @@ -3206,6 +3205,7 @@ dependencies = [ name = "iroha_executor_data_model" version = "2.0.0-rc.1.0" dependencies = [ + "derive_more", "iroha_data_model", "iroha_executor_data_model_derive", "iroha_schema", @@ -3364,17 +3364,6 @@ dependencies = [ "syn 2.0.75", ] -[[package]] -name = "iroha_multisig_data_model" -version = "2.0.0-rc.1.0" -dependencies = [ - "iroha_data_model", - "iroha_schema", - "parity-scale-codec", - "serde", - "serde_json", -] - [[package]] name = "iroha_numeric" version = "2.0.0-rc.1.0" @@ -3480,7 +3469,6 @@ dependencies = [ "iroha_data_model", "iroha_executor_data_model", "iroha_genesis", - "iroha_multisig_data_model", "iroha_primitives", "iroha_schema", ] diff --git a/Cargo.toml b/Cargo.toml index f0e087d5966..740812a3f2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,7 +44,6 @@ iroha_smart_contract_utils = { version = "=2.0.0-rc.1.0", path = "crates/iroha_s iroha_executor = { version = "=2.0.0-rc.1.0", path = "crates/iroha_executor" } iroha_data_model = { version = "=2.0.0-rc.1.0", path = "crates/iroha_data_model", default-features = false } -iroha_multisig_data_model = { version = "=2.0.0-rc.1.0", path = "data_model/libs/iroha_multisig_data_model" } iroha_executor_data_model = { version = "=2.0.0-rc.1.0", path = "crates/iroha_executor_data_model" } iroha_test_network = { version = "=2.0.0-rc.1.0", path = "crates/iroha_test_network" } @@ -199,7 +198,6 @@ clippy.wildcard_dependencies = "deny" resolver = "2" members = [ "crates/*", - "data_model/libs/*", "data_model/samples/*" ] diff --git a/crates/iroha/Cargo.toml b/crates/iroha/Cargo.toml index 6990b70d363..88e705e6efc 100644 --- a/crates/iroha/Cargo.toml +++ b/crates/iroha/Cargo.toml @@ -58,7 +58,7 @@ iroha_torii_const = { workspace = true } iroha_version = { workspace = true } iroha_data_model = { workspace = true, features = ["http"] } -iroha_multisig_data_model = { workspace = true } +iroha_executor_data_model = { workspace = true } attohttpc = { version = "0.28.0", default-features = false } eyre = { workspace = true } @@ -88,7 +88,6 @@ iroha_test_network = { workspace = true } mint_rose_trigger_data_model = { path = "../../data_model/samples/mint_rose_trigger_data_model" } executor_custom_data_model = { path = "../../data_model/samples/executor_custom_data_model" } -iroha_executor_data_model = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread"] } reqwest = { version = "0.12.7", features = ["json"] } diff --git a/crates/iroha/src/lib.rs b/crates/iroha/src/lib.rs index 0daf1930e55..02c3553fb1d 100644 --- a/crates/iroha/src/lib.rs +++ b/crates/iroha/src/lib.rs @@ -9,4 +9,4 @@ mod secrecy; pub use iroha_crypto as crypto; pub use iroha_data_model as data_model; -pub use iroha_multisig_data_model as multisig_data_model; +pub use iroha_executor_data_model as executor_data_model; diff --git a/crates/iroha/tests/multisig.rs b/crates/iroha/tests/multisig.rs index 84c6c453f9a..1ac73daca89 100644 --- a/crates/iroha/tests/multisig.rs +++ b/crates/iroha/tests/multisig.rs @@ -1,77 +1,108 @@ use std::{ collections::{BTreeMap, BTreeSet}, + num::{NonZeroU16, NonZeroU64}, time::Duration, }; +use derive_more::Constructor; use eyre::Result; use iroha::{ client::Client, crypto::KeyPair, - data_model::{prelude::*, query::trigger::FindTriggers, Level}, + data_model::{prelude::*, Level}, + executor_data_model::isi::multisig::*, }; -use iroha_data_model::events::execute_trigger::ExecuteTriggerEventFilter; -use iroha_multisig_data_model::{MultisigAccountArgs, MultisigTransactionArgs}; +use iroha_executor_data_model::permission::account::CanRegisterAccount; use iroha_test_network::*; use iroha_test_samples::{ gen_account_in, ALICE_ID, BOB_ID, BOB_KEYPAIR, CARPENTER_ID, CARPENTER_KEYPAIR, }; +#[derive(Constructor)] +struct TestSuite { + domain: DomainId, + multisig_account_id: AccountId, + unauthorized_target_opt: Option, + transaction_ttl_ms_opt: Option, +} + #[test] -fn multisig() -> Result<()> { - multisig_base(None) +fn multisig_normal() -> Result<()> { + // New domain for this test + let domain = "kingdom".parse().unwrap(); + // Create a multisig account ID and discard the corresponding private key + // FIXME #5022 refuse user input to prevent multisig monopoly and pre-registration hijacking + let multisig_account_id = gen_account_in(&domain).0; + // Make some changes to the multisig account itself + let unauthorized_target_opt = None; + // Semi-permanently valid + let transaction_ttl_ms_opt = None; + + let suite = TestSuite::new( + domain, + multisig_account_id, + unauthorized_target_opt, + transaction_ttl_ms_opt, + ); + multisig_base(suite) +} + +#[test] +fn multisig_unauthorized() -> Result<()> { + let domain = "kingdom".parse().unwrap(); + let multisig_account_id = gen_account_in(&domain).0; + // Someone that the multisig account has no permission to access + let unauthorized_target_opt = Some(ALICE_ID.clone()); + + let suite = TestSuite::new(domain, multisig_account_id, unauthorized_target_opt, None); + multisig_base(suite) } #[test] fn multisig_expires() -> Result<()> { - multisig_base(Some(2)) + let domain = "kingdom".parse().unwrap(); + let multisig_account_id = gen_account_in(&domain).0; + // Expires after 1 sec + let transaction_ttl_ms_opt = Some(1_000); + + let suite = TestSuite::new(domain, multisig_account_id, None, transaction_ttl_ms_opt); + multisig_base(suite) } /// # Scenario /// -/// Proceeds from top left to bottom right. Starred operations are the responsibility of the user -/// -/// ``` -/// | world level | domain level | account level | transaction level | -/// |---------------------------|-----------------------------|---------------------------------|----------------------| -/// | given domains initializer | | | | -/// | | * creates domain | | | -/// | domains initializer | generates accounts registry | | | -/// | | | * creates signatories | | -/// | | * calls accounts registry | generates multisig account | | -/// | | accounts registry | generates transactions registry | | -/// | | | * calls transactions registry | proposes transaction | -/// | | | * calls transactions registry | approves transaction | -/// | | | transactions registry | executes transaction | -/// ``` -#[allow(clippy::cast_possible_truncation)] -fn multisig_base(transaction_ttl_ms: Option) -> Result<()> { +/// 1. Signatories are populated and ready to join a multisig account +/// 2. Someone in the domain registers a multisig account +/// 3. One of the signatories of the multisig account proposes a multisig transaction +/// 4. Other signatories approve the multisig transaction +/// 5. The multisig transaction executes when all of the following are met: +/// - Quorum reached: authenticated +/// - Transaction has not expired +/// - Every instruction validated against the multisig account: authorized +/// 6. Either execution or expiration on approval deletes the transaction entry +#[expect(clippy::cast_possible_truncation, clippy::too_many_lines)] +fn multisig_base(suite: TestSuite) -> Result<()> { const N_SIGNATORIES: usize = 5; + let TestSuite { + domain, + multisig_account_id, + unauthorized_target_opt, + transaction_ttl_ms_opt, + } = suite; + let (network, _rt) = NetworkBuilder::new().start_blocking()?; let test_client = network.client(); - let kingdom: DomainId = "kingdom".parse().unwrap(); - // Assume some domain registered after genesis let register_and_transfer_kingdom: [InstructionBox; 2] = [ - Register::domain(Domain::new(kingdom.clone())).into(), - Transfer::domain(ALICE_ID.clone(), kingdom.clone(), BOB_ID.clone()).into(), + Register::domain(Domain::new(domain.clone())).into(), + Transfer::domain(ALICE_ID.clone(), domain.clone(), BOB_ID.clone()).into(), ]; test_client.submit_all_blocking(register_and_transfer_kingdom)?; - // One more block to generate a multisig accounts registry for the domain - test_client.submit_blocking(Log::new(Level::DEBUG, "Just ticking time".to_string()))?; - - // Check that the multisig accounts registry has been generated - let multisig_accounts_registry_id = multisig_accounts_registry_of(&kingdom); - let _trigger = test_client - .query(FindTriggers::new()) - .filter_with(|trigger| trigger.id.eq(multisig_accounts_registry_id.clone())) - .execute_single() - .expect("multisig accounts registry should be generated after domain creation"); - // Populate residents in the domain - let mut residents = core::iter::repeat_with(|| gen_account_in(&kingdom)) + let mut residents = core::iter::repeat_with(|| gen_account_in(&domain)) .take(1 + N_SIGNATORIES) .collect::>(); alt_client((BOB_ID.clone(), BOB_KEYPAIR.clone()), &test_client).submit_all_blocking( @@ -82,25 +113,28 @@ fn multisig_base(transaction_ttl_ms: Option) -> Result<()> { .map(Register::account), )?; - // Create a multisig account ID and discard the corresponding private key - let multisig_account_id = gen_account_in(&kingdom).0; - - let not_signatory = residents.pop_first().unwrap(); + let non_signatory = residents.pop_first().unwrap(); let mut signatories = residents; - let args = &MultisigAccountArgs { - account: multisig_account_id.signatory().clone(), - signatories: signatories + let register_multisig_account = MultisigRegister::new( + multisig_account_id.clone(), + signatories .keys() .enumerate() .map(|(weight, id)| (id.clone(), 1 + weight as u8)) .collect(), - // Can be met without the first signatory - quorum: (1..=N_SIGNATORIES).skip(1).sum::() as u16, - transaction_ttl_ms: transaction_ttl_ms.unwrap_or(u64::MAX), - }; - let register_multisig_account = - ExecuteTrigger::new(multisig_accounts_registry_id).with_args(args); + // Quorum can be reached without the first signatory + (1..=N_SIGNATORIES) + .skip(1) + .sum::() + .try_into() + .ok() + .and_then(NonZeroU16::new) + .unwrap(), + transaction_ttl_ms_opt + .and_then(NonZeroU64::new) + .unwrap_or(NonZeroU64::MAX), + ); // Any account in another domain cannot register a multisig account without special permission let _err = alt_client( @@ -110,75 +144,111 @@ fn multisig_base(transaction_ttl_ms: Option) -> Result<()> { .submit_blocking(register_multisig_account.clone()) .expect_err("multisig account should not be registered by account of another domain"); - // Any account in the same domain can register a multisig account without special permission - alt_client(not_signatory, &test_client) + // Non-signatory account in the same domain cannot register a multisig account without special permission + let _err = alt_client(non_signatory.clone(), &test_client) + .submit_blocking(register_multisig_account.clone()) + .expect_err( + "multisig account should not be registered by non-signatory account of the same domain", + ); + + // All but the first signatory approve the proposal + let signatory = signatories.pop_first().unwrap(); + + // Signatory account cannot register a multisig account without special permission + let _err = alt_client(signatory, &test_client) + .submit_blocking(register_multisig_account.clone()) + .expect_err("multisig account should not be registered by signatory account"); + + // Account with permission can register a multisig account + alt_client((BOB_ID.clone(), BOB_KEYPAIR.clone()), &test_client).submit_blocking( + Grant::account_permission(CanRegisterAccount { domain }, non_signatory.0.clone()), + )?; + alt_client(non_signatory, &test_client) .submit_blocking(register_multisig_account) - .expect("multisig account should be registered by account of the same domain"); + .expect("multisig account should be registered by account with permission"); // Check that the multisig account has been registered test_client .query(FindAccounts::new()) .filter_with(|account| account.id.eq(multisig_account_id.clone())) .execute_single() - .expect("multisig account should be created by calling the multisig accounts registry"); - - // Check that the multisig transactions registry has been generated - let multisig_transactions_registry_id = multisig_transactions_registry_of(&multisig_account_id); - let _trigger = test_client - .query(FindTriggers::new()) - .filter_with(|trigger| trigger.id.eq(multisig_transactions_registry_id.clone())) - .execute_single() - .expect("multisig transactions registry should be generated along with the corresponding multisig account"); + .expect("multisig account should be created"); - let key: Name = "key".parse().unwrap(); + let key: Name = "success_marker".parse().unwrap(); + let transaction_target = unauthorized_target_opt + .as_ref() + .unwrap_or(&multisig_account_id) + .clone(); let instructions = vec![SetKeyValue::account( - multisig_account_id.clone(), + transaction_target.clone(), key.clone(), - "value".parse::().unwrap(), + "congratulations".parse::().unwrap(), ) .into()]; let instructions_hash = HashOf::new(&instructions); let proposer = signatories.pop_last().unwrap(); - let approvers = signatories; - - let args = &MultisigTransactionArgs::Propose(instructions); - let propose = ExecuteTrigger::new(multisig_transactions_registry_id.clone()).with_args(args); + let mut approvers = signatories.into_iter(); + let propose = MultisigPropose::new(multisig_account_id.clone(), instructions); alt_client(proposer, &test_client).submit_blocking(propose)?; - // Check that the multisig transaction has not yet executed - let _err = test_client - .query_single(FindAccountMetadata::new( - multisig_account_id.clone(), - key.clone(), - )) - .expect_err("key-value shouldn't be set without enough approvals"); - // Allow time to elapse to test the expiration - if let Some(ms) = transaction_ttl_ms { + if let Some(ms) = transaction_ttl_ms_opt { std::thread::sleep(Duration::from_millis(ms)) }; test_client.submit_blocking(Log::new(Level::DEBUG, "Just ticking time".to_string()))?; - // All but the first signatory approve the multisig transaction - for approver in approvers.into_iter().skip(1) { - let args = &MultisigTransactionArgs::Approve(instructions_hash); - let approve = - ExecuteTrigger::new(multisig_transactions_registry_id.clone()).with_args(args); + let approve = MultisigApprove::new(multisig_account_id.clone(), instructions_hash); + + // Approve once to see if the proposal expires + let approver = approvers.next().unwrap(); + alt_client(approver, &test_client).submit_blocking(approve.clone())?; - alt_client(approver, &test_client).submit_blocking(approve)?; + // Subsequent approvals should succeed unless the proposal is expired + for _ in 0..(N_SIGNATORIES - 4) { + let approver = approvers.next().unwrap(); + let res = alt_client(approver, &test_client).submit_blocking(approve.clone()); + match &transaction_ttl_ms_opt { + None => assert!(res.is_ok()), + _ => assert!(res.is_err()), + } } - // Check that the multisig transaction has executed + + // Check that the multisig transaction has not yet executed + let _err = test_client + .query_single(FindAccountMetadata::new( + transaction_target.clone(), + key.clone(), + )) + .expect_err("instructions shouldn't execute without enough approvals"); + + // The last approve to proceed to validate and execute the instructions + let approver = approvers.next().unwrap(); + let res = alt_client(approver, &test_client).submit_blocking(approve.clone()); + match (&transaction_ttl_ms_opt, &unauthorized_target_opt) { + (None, None) => assert!(res.is_ok()), + _ => assert!(res.is_err()), + } + + // Check if the multisig transaction has executed + let res = test_client.query_single(FindAccountMetadata::new(transaction_target, key.clone())); + match (&transaction_ttl_ms_opt, &unauthorized_target_opt) { + (None, None) => assert!(res.is_ok()), + _ => assert!(res.is_err()), + } + + // Check if the transaction entry is deleted let res = test_client.query_single(FindAccountMetadata::new( - multisig_account_id.clone(), - key.clone(), + multisig_account_id, + format!("proposals/{instructions_hash}/instructions") + .parse() + .unwrap(), )); - - if transaction_ttl_ms.is_some() { - let _err = res.expect_err("key-value shouldn't be set despite enough approvals"); - } else { - res.expect("key-value should be set with enough approvals"); + match (&transaction_ttl_ms_opt, &unauthorized_target_opt) { + // In case failing validation, the entry can exit only by expiring + (None, Some(_)) => assert!(res.is_ok()), + _ => assert!(res.is_err()), } Ok(()) @@ -196,13 +266,12 @@ fn multisig_base(transaction_ttl_ms: Option) -> Result<()> { /// 0 1 2 3 4 5 <--- personal signatories /// ``` #[test] -#[allow(clippy::similar_names, clippy::too_many_lines)] +#[expect(clippy::similar_names, clippy::too_many_lines)] fn multisig_recursion() -> Result<()> { let (network, _rt) = NetworkBuilder::new().start_blocking()?; let test_client = network.client(); let wonderland = "wonderland"; - let ms_accounts_registry_id = multisig_accounts_registry_of(&wonderland.parse().unwrap()); // Populate signatories in the domain let signatories = core::iter::repeat_with(|| gen_account_in(wonderland)) @@ -227,14 +296,16 @@ fn multisig_recursion() -> Result<()> { .into_iter() .map(|sigs| { let ms_account_id = gen_account_in(wonderland).0; - let args = MultisigAccountArgs { - account: ms_account_id.signatory().clone(), - signatories: sigs.iter().copied().map(|id| (id.clone(), 1)).collect(), - quorum: sigs.len().try_into().unwrap(), - transaction_ttl_ms: u64::MAX, - }; - let register_ms_account = - ExecuteTrigger::new(ms_accounts_registry_id.clone()).with_args(&args); + let register_ms_account = MultisigRegister::new( + ms_account_id.clone(), + sigs.iter().copied().map(|id| (id.clone(), 1)).collect(), + sigs.len() + .try_into() + .ok() + .and_then(NonZeroU16::new) + .unwrap(), + NonZeroU64::new(u64::MAX).unwrap(), + ); test_client .submit_blocking(register_ms_account) @@ -264,52 +335,37 @@ fn multisig_recursion() -> Result<()> { let msa_012345 = msas[0].clone(); // One of personal signatories proposes a multisig transaction - let key: Name = "key".parse().unwrap(); + let key: Name = "success_marker".parse().unwrap(); let instructions = vec![SetKeyValue::account( msa_012345.clone(), key.clone(), - "value".parse::().unwrap(), + "congratulations".parse::().unwrap(), ) .into()]; let instructions_hash = HashOf::new(&instructions); let proposer = sigs_0.pop_last().unwrap(); - let ms_transactions_registry_id = multisig_transactions_registry_of(&msa_012345); - let args = MultisigTransactionArgs::Propose(instructions); - let propose = ExecuteTrigger::new(ms_transactions_registry_id.clone()).with_args(&args); + let propose = MultisigPropose::new(msa_012345.clone(), instructions); alt_client(proposer, &test_client).submit_blocking(propose)?; - // Ticks as many times as the multisig recursion - (0..2).for_each(|_| { - test_client - .submit_blocking(Log::new(Level::DEBUG, "Just ticking time".to_string())) - .unwrap(); - }); - - // Check that the entire authentication policy has been deployed down to one of the leaf registries + // Check that the entire authentication policy has been deployed down to one of the leaf signatories let approval_hash_to_12345 = { let approval_hash_to_012345 = { - let registry_id = multisig_transactions_registry_of(&msa_012345); - let args = MultisigTransactionArgs::Approve(instructions_hash); - let approve: InstructionBox = ExecuteTrigger::new(registry_id.clone()) - .with_args(&args) - .into(); + let approve: InstructionBox = + MultisigApprove::new(msa_012345.clone(), instructions_hash).into(); HashOf::new(&vec![approve]) }; - let registry_id = multisig_transactions_registry_of(&msa_12345); - let args = MultisigTransactionArgs::Approve(approval_hash_to_012345); - let approve: InstructionBox = ExecuteTrigger::new(registry_id.clone()) - .with_args(&args) - .into(); + let approve: InstructionBox = + MultisigApprove::new(msa_12345.clone(), approval_hash_to_012345).into(); HashOf::new(&vec![approve]) }; let approvals_at_12: BTreeSet = test_client - .query_single(FindTriggerMetadata::new( - multisig_transactions_registry_of(&msa_12), + .query_single(FindAccountMetadata::new( + msa_12.clone(), format!("proposals/{approval_hash_to_12345}/approvals") .parse() .unwrap(), @@ -323,16 +379,14 @@ fn multisig_recursion() -> Result<()> { // Check that the multisig transaction has not yet executed let _err = test_client .query_single(FindAccountMetadata::new(msa_012345.clone(), key.clone())) - .expect_err("key-value shouldn't be set without enough approvals"); + .expect_err("instructions shouldn't execute without enough approvals"); // All the rest signatories approve the multisig transaction let approve_for_each = |approvers: BTreeMap, instructions_hash: HashOf>, ms_account: &AccountId| { for approver in approvers { - let registry_id = multisig_transactions_registry_of(ms_account); - let args = MultisigTransactionArgs::Approve(instructions_hash); - let approve = ExecuteTrigger::new(registry_id.clone()).with_args(&args); + let approve = MultisigApprove::new(ms_account.clone(), instructions_hash); alt_client(approver, &test_client) .submit_blocking(approve) @@ -343,47 +397,10 @@ fn multisig_recursion() -> Result<()> { approve_for_each(sigs_12, approval_hash_to_12345, &msa_12); approve_for_each(sigs_345, approval_hash_to_12345, &msa_345); - // Let the intermediate registry (12345) collect approvals and approve the original proposal - test_client.submit_blocking(Log::new(Level::DEBUG, "Just ticking time".to_string()))?; - - // Let the root registry (012345) collect approvals and execute the original proposal - test_client.submit_blocking(Log::new(Level::DEBUG, "Just ticking time".to_string()))?; - // Check that the multisig transaction has executed test_client .query_single(FindAccountMetadata::new(msa_012345.clone(), key.clone())) - .expect("key-value should be set with enough approvals"); - - Ok(()) -} - -#[test] -fn persistent_domain_level_authority() -> Result<()> { - let (network, _rt) = NetworkBuilder::new().start_blocking()?; - let test_client = network.client(); - - let wonderland: DomainId = "wonderland".parse().unwrap(); - - let ms_accounts_registry_id = multisig_accounts_registry_of(&wonderland); - - // Domain owner changes from Alice to Bob - test_client.submit_blocking(Transfer::domain( - ALICE_ID.clone(), - wonderland, - BOB_ID.clone(), - ))?; - - // One block gap to follow the domain owner change - test_client.submit_blocking(Log::new(Level::DEBUG, "Just ticking time".to_string()))?; - - // Bob is the authority of the wonderland multisig accounts registry - let ms_accounts_registry = test_client - .query(FindTriggers::new()) - .filter_with(|trigger| trigger.id.eq(ms_accounts_registry_id.clone())) - .execute_single() - .expect("multisig accounts registry should survive before and after a domain owner change"); - - assert!(*ms_accounts_registry.action().authority() == BOB_ID.clone()); + .expect("instructions should execute with enough approvals"); Ok(()) } @@ -396,61 +413,18 @@ fn reserved_names() { let account_in_another_domain = gen_account_in("garden_of_live_flowers").0; { - let reserved_prefix = "multisig_accounts_"; - let register = { - let id: TriggerId = format!("{reserved_prefix}{}", account_in_another_domain.domain()) - .parse() - .unwrap(); - let action = Action::new( - Vec::::new(), - Repeats::Indefinitely, - ALICE_ID.clone(), - ExecuteTriggerEventFilter::new(), - ); - Register::trigger(Trigger::new(id, action)) - }; - let _err = test_client.submit_blocking(register).expect_err( - "trigger with this name shouldn't be registered by anyone other than multisig system", - ); - } - - { - let reserved_prefix = "multisig_transactions_"; - let register = { - let id: TriggerId = format!( - "{reserved_prefix}{}_{}", - account_in_another_domain.signatory(), - account_in_another_domain.domain() - ) - .parse() - .unwrap(); - let action = Action::new( - Vec::::new(), - Repeats::Indefinitely, - ALICE_ID.clone(), - ExecuteTriggerEventFilter::new(), - ); - Register::trigger(Trigger::new(id, action)) - }; - let _err = test_client.submit_blocking(register).expect_err( - "trigger with this name shouldn't be registered by anyone other than domain owner", - ); - } - - { - let reserved_prefix = "multisig_signatory_"; let register = { - let id: RoleId = format!( - "{reserved_prefix}{}_{}", - account_in_another_domain.signatory(), - account_in_another_domain.domain() + let role = format!( + "MULTISIG_SIGNATORY/{}/{}", + account_in_another_domain.domain(), + account_in_another_domain.signatory() ) .parse() .unwrap(); - Register::role(Role::new(id, ALICE_ID.clone())) + Register::role(Role::new(role, ALICE_ID.clone())) }; let _err = test_client.submit_blocking(register).expect_err( - "role with this name shouldn't be registered by anyone other than domain owner", + "role with this name shouldn't be registered by anyone other than the domain owner", ); } } @@ -463,28 +437,13 @@ fn alt_client(signatory: (AccountId, KeyPair), base_client: &Client) -> Client { } } -fn multisig_accounts_registry_of(domain: &DomainId) -> TriggerId { - format!("multisig_accounts_{domain}",).parse().unwrap() -} - -fn multisig_transactions_registry_of(multisig_account: &AccountId) -> TriggerId { - format!( - "multisig_transactions_{}_{}", - multisig_account.signatory(), - multisig_account.domain() - ) - .parse() - .unwrap() -} - -#[allow(dead_code)] -fn debug_mst_registry(msa: &AccountId, client: &Client) { - let mst_registry = client - .query(FindTriggers::new()) - .filter_with(|trigger| trigger.id.eq(multisig_transactions_registry_of(msa))) +#[expect(dead_code)] +fn debug_account(account_id: &AccountId, client: &Client) { + let account = client + .query(FindAccounts) + .filter_with(|account| account.id.eq(account_id.clone())) .execute_single() .unwrap(); - let mst_metadata = mst_registry.action().metadata(); - iroha_logger::error!(%msa, ?mst_metadata); + iroha_logger::error!(?account); } diff --git a/crates/iroha_cli/src/main.rs b/crates/iroha_cli/src/main.rs index 41c8d252843..c0fb1e9761e 100644 --- a/crates/iroha_cli/src/main.rs +++ b/crates/iroha_cli/src/main.rs @@ -1177,12 +1177,13 @@ mod json { } mod multisig { - use std::io::{BufReader, Read as _}; - - use iroha::multisig_data_model::{ - MultisigAccountArgs, MultisigTransactionArgs, DEFAULT_MULTISIG_TTL_MS, + use std::{ + io::{BufReader, Read as _}, + num::{NonZeroU16, NonZeroU64}, }; + use iroha::executor_data_model::isi::multisig::*; + use super::*; /// Arguments for multisig subcommand @@ -1190,7 +1191,7 @@ mod multisig { pub enum Args { /// Register a multisig account Register(Register), - /// Propose a multisig transaction + /// Propose a multisig transaction, with `Vec` stdin Propose(Propose), /// Approve a multisig transaction Approve(Approve), @@ -1230,30 +1231,20 @@ mod multisig { impl RunArgs for Register { fn run(self, context: &mut dyn RunContext) -> Result<()> { - let Self { - account, - signatories, - weights, - quorum, - transaction_ttl, - } = self; - if signatories.len() != weights.len() { + if self.signatories.len() != self.weights.len() { return Err(eyre!("signatories and weights must be equal in length")); } - let registry_id: TriggerId = format!("multisig_accounts_{}", account.domain()) - .parse() - .unwrap(); - let args = MultisigAccountArgs { - account: account.signatory().clone(), - signatories: signatories.into_iter().zip(weights).collect(), - quorum, - transaction_ttl_ms: transaction_ttl + let register_multisig_account = MultisigRegister::new( + self.account, + self.signatories.into_iter().zip(self.weights).collect(), + NonZeroU16::new(self.quorum).expect("quorum should not be 0"), + self.transaction_ttl .as_millis() .try_into() - .expect("ttl must be within 584942417 years"), - }; - let register_multisig_account = - iroha::data_model::isi::ExecuteTrigger::new(registry_id).with_args(&args); + .ok() + .and_then(NonZeroU64::new) + .expect("ttl should be between 1 ms and 584942417 years"), + ); submit([register_multisig_account], Metadata::default(), context) .wrap_err("Failed to register multisig account") @@ -1270,14 +1261,6 @@ mod multisig { impl RunArgs for Propose { fn run(self, context: &mut dyn RunContext) -> Result<()> { - let Self { account } = self; - let registry_id: TriggerId = format!( - "multisig_transactions_{}_{}", - account.signatory(), - account.domain() - ) - .parse() - .unwrap(); let instructions: Vec = { let mut reader = BufReader::new(stdin()); let mut raw_content = Vec::new(); @@ -1287,9 +1270,7 @@ mod multisig { }; let instructions_hash = HashOf::new(&instructions); println!("{instructions_hash}"); - let args = MultisigTransactionArgs::Propose(instructions); - let propose_multisig_transaction = - iroha::data_model::isi::ExecuteTrigger::new(registry_id).with_args(&args); + let propose_multisig_transaction = MultisigPropose::new(self.account, instructions); submit([propose_multisig_transaction], Metadata::default(), context) .wrap_err("Failed to propose transaction") @@ -1309,20 +1290,8 @@ mod multisig { impl RunArgs for Approve { fn run(self, context: &mut dyn RunContext) -> Result<()> { - let Self { - account, - instructions_hash, - } = self; - let registry_id: TriggerId = format!( - "multisig_transactions_{}_{}", - account.signatory(), - account.domain() - ) - .parse() - .unwrap(); - let args = MultisigTransactionArgs::Approve(instructions_hash); let approve_multisig_transaction = - iroha::data_model::isi::ExecuteTrigger::new(registry_id).with_args(&args); + MultisigApprove::new(self.account, self.instructions_hash); submit([approve_multisig_transaction], Metadata::default(), context) .wrap_err("Failed to approve transaction") @@ -1345,6 +1314,22 @@ mod multisig { } } + const DELIMITER: char = '/'; + const PROPOSALS: &str = "proposals"; + const MULTISIG_SIGNATORY: &str = "MULTISIG_SIGNATORY"; + + fn multisig_account_from(role: &RoleId) -> Option { + role.name() + .as_ref() + .strip_prefix(MULTISIG_SIGNATORY)? + .rsplit_once(DELIMITER) + .and_then(|(init, last)| { + format!("{last}@{}", init.trim_matches(DELIMITER)) + .parse() + .ok() + }) + } + /// Recursively trace back to the root multisig account fn trace_back_from( account: AccountId, @@ -1353,42 +1338,27 @@ mod multisig { ) -> Result<()> { let Ok(multisig_roles) = client .query(FindRolesByAccountId::new(account)) - .filter_with(|role_id| role_id.name.starts_with("multisig_signatory_")) + .filter_with(|role_id| role_id.name.starts_with(MULTISIG_SIGNATORY)) .execute_all() else { return Ok(()); }; for role_id in multisig_roles { - let super_account: AccountId = role_id - .name() - .as_ref() - .strip_prefix("multisig_signatory_") - .unwrap() - .replacen('_', "@", 1) - .parse() - .unwrap(); - - trace_back_from(super_account, client, context)?; - - let transactions_registry_id: TriggerId = role_id - .name() - .as_ref() - .replace("signatory", "transactions") - .parse() - .unwrap(); - - context.print_data(&transactions_registry_id)?; - - let transactions_registry = client - .query(FindTriggers::new()) - .filter_with(|trigger| trigger.id.eq(transactions_registry_id)) + let super_account_id: AccountId = multisig_account_from(&role_id).unwrap(); + + trace_back_from(super_account_id.clone(), client, context)?; + + context.print_data(&super_account_id)?; + + let super_account = client + .query(FindAccounts) + .filter_with(|account| account.id.eq(super_account_id)) .execute_single()?; - let proposal_kvs = transactions_registry - .action() + let proposal_kvs = super_account .metadata() .iter() - .filter(|kv| kv.0.as_ref().starts_with("proposals")); + .filter(|kv| kv.0.as_ref().starts_with(PROPOSALS)); proposal_kvs.fold("", |acc, (k, v)| { let mut path = k.as_ref().split('/'); diff --git a/crates/iroha_data_model/src/visit.rs b/crates/iroha_data_model/src/visit.rs index 92b8709c618..dae06cd82cd 100644 --- a/crates/iroha_data_model/src/visit.rs +++ b/crates/iroha_data_model/src/visit.rs @@ -49,7 +49,7 @@ pub trait Visit { visit_execute_trigger(&ExecuteTrigger), visit_set_parameter(&SetParameter), visit_log(&Log), - visit_custom(&CustomInstruction), + visit_custom_instruction(&CustomInstruction), // Visit SingularQueryBox visit_find_asset_quantity_by_id(&FindAssetQuantityById), @@ -230,7 +230,7 @@ pub fn visit_instruction(visitor: &mut V, isi: &InstructionBo InstructionBox::Transfer(variant_value) => visitor.visit_transfer(variant_value), InstructionBox::Unregister(variant_value) => visitor.visit_unregister(variant_value), InstructionBox::Upgrade(variant_value) => visitor.visit_upgrade(variant_value), - InstructionBox::Custom(custom) => visitor.visit_custom(custom), + InstructionBox::Custom(custom) => visitor.visit_custom_instruction(custom), } } @@ -373,7 +373,7 @@ leaf_visitors! { visit_set_parameter(&SetParameter), visit_execute_trigger(&ExecuteTrigger), visit_log(&Log), - visit_custom(&CustomInstruction), + visit_custom_instruction(&CustomInstruction), // Singular Quert visitors visit_find_asset_quantity_by_id(&FindAssetQuantityById), diff --git a/crates/iroha_executor/src/default/isi/mod.rs b/crates/iroha_executor/src/default/isi/mod.rs new file mode 100644 index 00000000000..d3fddf374c0 --- /dev/null +++ b/crates/iroha_executor/src/default/isi/mod.rs @@ -0,0 +1,49 @@ +use iroha_executor_data_model::isi::multisig::MultisigInstructionBox; + +use super::*; +use crate::prelude::{Execute, Vec, Visit}; + +pub fn visit_custom_instruction( + executor: &mut V, + instruction: &CustomInstruction, +) { + if let Ok(instruction) = MultisigInstructionBox::try_from(instruction.payload()) { + return instruction.visit_execute(executor); + }; + + deny!(executor, "unexpected custom instruction"); +} + +trait VisitExecute: crate::data_model::isi::Instruction { + fn visit_execute(self, executor: &mut V) { + let init_authority = executor.context().authority.clone(); + self.visit(executor); + if executor.verdict().is_ok() { + if let Err(err) = self.execute(executor) { + executor.deny(err); + } + } + executor.context_mut().authority = init_authority; + } + + fn visit(&self, _executor: &mut V) { + unimplemented!("should be overridden unless `Self::visit_execute` is overridden") + } + + fn execute(self, _executor: &mut V) -> Result<(), ValidationFail> { + unimplemented!("should be overridden unless `Self::visit_execute` is overridden") + } +} + +/// Validate and execute instructions in sequence without returning back to the visit root, +/// checking the sanity of the executor verdict +macro_rules! visit_seq { + ($executor:ident.$visit:ident($instruction:expr)) => { + $executor.$visit($instruction); + if $executor.verdict().is_err() { + return $executor.verdict().clone(); + } + }; +} + +mod multisig; diff --git a/crates/iroha_executor/src/default/isi/multisig/account.rs b/crates/iroha_executor/src/default/isi/multisig/account.rs new file mode 100644 index 00000000000..1b27223b93f --- /dev/null +++ b/crates/iroha_executor/src/default/isi/multisig/account.rs @@ -0,0 +1,64 @@ +//! Validation and execution logic of instructions for multisig accounts + +use super::*; + +impl VisitExecute for MultisigRegister { + fn visit(&self, _executor: &mut V) {} + + fn execute(self, executor: &mut V) -> Result<(), ValidationFail> { + let multisig_account = self.account; + let multisig_role = multisig_role_for(&multisig_account); + + // The multisig registrant needs to have sufficient permission to register personal accounts + // TODO Loosen to just being one of the signatories? But impose the procedure of propose and approve? + visit_seq!(executor + .visit_register_account(&Register::account(Account::new(multisig_account.clone())))); + + let domain_owner = executor + .host() + .query(FindDomains) + .filter_with(|domain| domain.id.eq(multisig_account.domain().clone())) + .execute_single() + .dbg_expect("domain should be found as the preceding account registration succeeded") + .owned_by() + .clone(); + + // Authorize as the domain owner: + // Just having permission to register accounts is insufficient to register multisig roles + executor.context_mut().authority = domain_owner.clone(); + + visit_seq!(executor.visit_set_account_key_value(&SetKeyValue::account( + multisig_account.clone(), + SIGNATORIES.parse().unwrap(), + Json::new(&self.signatories), + ))); + + visit_seq!(executor.visit_set_account_key_value(&SetKeyValue::account( + multisig_account.clone(), + QUORUM.parse().unwrap(), + Json::new(self.quorum), + ))); + + visit_seq!(executor.visit_set_account_key_value(&SetKeyValue::account( + multisig_account.clone(), + TRANSACTION_TTL_MS.parse().unwrap(), + Json::new(self.transaction_ttl_ms), + ))); + + visit_seq!(executor.visit_register_role(&Register::role( + // Temporarily grant a multisig role to the domain owner to delegate the role to the signatories + Role::new(multisig_role.clone(), domain_owner.clone()), + ))); + + for signatory in self.signatories.keys().cloned() { + visit_seq!(executor + .visit_grant_account_role(&Grant::account_role(multisig_role.clone(), signatory))); + } + + visit_seq!( + executor.visit_revoke_account_role(&Revoke::account_role(multisig_role, domain_owner)) + ); + + Ok(()) + } +} diff --git a/crates/iroha_executor/src/default/isi/multisig/mod.rs b/crates/iroha_executor/src/default/isi/multisig/mod.rs new file mode 100644 index 00000000000..6ed09717677 --- /dev/null +++ b/crates/iroha_executor/src/default/isi/multisig/mod.rs @@ -0,0 +1,52 @@ +use iroha_executor_data_model::isi::multisig::*; + +use super::*; +use crate::smart_contract::{DebugExpectExt as _, DebugUnwrapExt}; + +mod account; +mod transaction; + +impl VisitExecute for MultisigInstructionBox { + fn visit_execute(self, executor: &mut V) { + match self { + MultisigInstructionBox::Register(instruction) => instruction.visit_execute(executor), + MultisigInstructionBox::Propose(instruction) => instruction.visit_execute(executor), + MultisigInstructionBox::Approve(instruction) => instruction.visit_execute(executor), + } + } +} + +const DELIMITER: char = '/'; +const SIGNATORIES: &str = "signatories"; +const QUORUM: &str = "quorum"; +const TRANSACTION_TTL_MS: &str = "transaction_ttl_ms"; +const PROPOSALS: &str = "proposals"; +const MULTISIG_SIGNATORY: &str = "MULTISIG_SIGNATORY"; + +fn instructions_key(hash: &HashOf>) -> Name { + format!("{PROPOSALS}{DELIMITER}{hash}{DELIMITER}instructions") + .parse() + .unwrap() +} + +fn proposed_at_ms_key(hash: &HashOf>) -> Name { + format!("{PROPOSALS}{DELIMITER}{hash}{DELIMITER}proposed_at_ms") + .parse() + .unwrap() +} + +fn approvals_key(hash: &HashOf>) -> Name { + format!("{PROPOSALS}{DELIMITER}{hash}{DELIMITER}approvals") + .parse() + .unwrap() +} + +fn multisig_role_for(account: &AccountId) -> RoleId { + format!( + "{MULTISIG_SIGNATORY}{DELIMITER}{}{DELIMITER}{}", + account.domain(), + account.signatory(), + ) + .parse() + .unwrap() +} diff --git a/crates/iroha_executor/src/default/isi/multisig/transaction.rs b/crates/iroha_executor/src/default/isi/multisig/transaction.rs new file mode 100644 index 00000000000..542fcc1636b --- /dev/null +++ b/crates/iroha_executor/src/default/isi/multisig/transaction.rs @@ -0,0 +1,249 @@ +//! Validation and execution logic of instructions for multisig transactions + +use alloc::collections::{btree_map::BTreeMap, btree_set::BTreeSet}; + +use super::*; + +impl VisitExecute for MultisigPropose { + fn visit(&self, executor: &mut V) { + let proposer = executor.context().authority.clone(); + let multisig_account = self.account.clone(); + let host = executor.host(); + let instructions_hash = HashOf::new(&self.instructions); + let multisig_role = multisig_role_for(&multisig_account); + let is_downward_proposal = host + .query_single(FindAccountMetadata::new( + proposer.clone(), + SIGNATORIES.parse().unwrap(), + )) + .map_or(false, |proposer_signatories| { + proposer_signatories + .try_into_any::>() + .dbg_unwrap() + .contains_key(&multisig_account) + }); + let has_multisig_role = host + .query(FindRolesByAccountId::new(proposer)) + .filter_with(|role_id| role_id.eq(multisig_role)) + .execute_single() + .is_ok(); + + if !(is_downward_proposal || has_multisig_role) { + deny!(executor, "not qualified to propose multisig"); + }; + + if host + .query_single(FindAccountMetadata::new( + multisig_account.clone(), + approvals_key(&instructions_hash), + )) + .is_ok() + { + deny!(executor, "multisig proposal duplicates") + }; + } + + fn execute(self, executor: &mut V) -> Result<(), ValidationFail> { + let proposer = executor.context().authority.clone(); + let multisig_account = self.account; + + // Authorize as the multisig account + executor.context_mut().authority = multisig_account.clone(); + + let instructions_hash = HashOf::new(&self.instructions); + let signatories: BTreeMap = executor + .host() + .query_single(FindAccountMetadata::new( + multisig_account.clone(), + SIGNATORIES.parse().unwrap(), + )) + .dbg_unwrap() + .try_into_any() + .dbg_unwrap(); + let now_ms: u64 = executor + .context() + .curr_block + .creation_time() + .as_millis() + .try_into() + .dbg_expect("shouldn't overflow within 584942417 years"); + let approvals = BTreeSet::from([proposer]); + + // Recursively deploy multisig authentication down to the personal leaf signatories + for signatory in signatories.keys().cloned() { + let is_multisig_again = executor + .host() + .query(FindRoleIds) + .filter_with(|role_id| role_id.eq(multisig_role_for(&signatory))) + .execute_single_opt() + .dbg_unwrap() + .is_some(); + + if is_multisig_again { + let propose_to_approve_me = { + let approve_me = + MultisigApprove::new(multisig_account.clone(), instructions_hash); + + MultisigPropose::new(signatory, [approve_me.into()].to_vec()) + }; + + propose_to_approve_me.visit_execute(executor); + } + } + + visit_seq!(executor.visit_set_account_key_value(&SetKeyValue::account( + multisig_account.clone(), + instructions_key(&instructions_hash).clone(), + Json::new(&self.instructions), + ))); + + visit_seq!(executor.visit_set_account_key_value(&SetKeyValue::account( + multisig_account.clone(), + proposed_at_ms_key(&instructions_hash).clone(), + Json::new(now_ms), + ))); + + visit_seq!(executor.visit_set_account_key_value(&SetKeyValue::account( + multisig_account, + approvals_key(&instructions_hash).clone(), + Json::new(&approvals), + ))); + + Ok(()) + } +} + +impl VisitExecute for MultisigApprove { + fn visit(&self, executor: &mut V) { + let approver = executor.context().authority.clone(); + let multisig_account = self.account.clone(); + let host = executor.host(); + let multisig_role = multisig_role_for(&multisig_account); + + if host + .query(FindRolesByAccountId::new(approver)) + .filter_with(|role_id| role_id.eq(multisig_role)) + .execute_single() + .is_err() + { + deny!(executor, "not qualified to approve multisig"); + }; + } + + fn execute(self, executor: &mut V) -> Result<(), ValidationFail> { + let approver = executor.context().authority.clone(); + let multisig_account = self.account; + + // Authorize as the multisig account + executor.context_mut().authority = multisig_account.clone(); + + let host = executor.host(); + let instructions_hash = self.instructions_hash; + let signatories: BTreeMap = host + .query_single(FindAccountMetadata::new( + multisig_account.clone(), + SIGNATORIES.parse().unwrap(), + )) + .dbg_unwrap() + .try_into_any() + .dbg_unwrap(); + let quorum: u16 = host + .query_single(FindAccountMetadata::new( + multisig_account.clone(), + QUORUM.parse().unwrap(), + )) + .dbg_unwrap() + .try_into_any() + .dbg_unwrap(); + let transaction_ttl_ms: u64 = host + .query_single(FindAccountMetadata::new( + multisig_account.clone(), + TRANSACTION_TTL_MS.parse().unwrap(), + )) + .dbg_unwrap() + .try_into_any() + .dbg_unwrap(); + let instructions: Vec = host + .query_single(FindAccountMetadata::new( + multisig_account.clone(), + instructions_key(&instructions_hash), + ))? + .try_into_any() + .dbg_unwrap(); + let proposed_at_ms: u64 = host + .query_single(FindAccountMetadata::new( + multisig_account.clone(), + proposed_at_ms_key(&instructions_hash), + )) + .dbg_unwrap() + .try_into_any() + .dbg_unwrap(); + let now_ms: u64 = executor + .context() + .curr_block + .creation_time() + .as_millis() + .try_into() + .dbg_expect("shouldn't overflow within 584942417 years"); + let mut approvals: BTreeSet = host + .query_single(FindAccountMetadata::new( + multisig_account.clone(), + approvals_key(&instructions_hash), + )) + .dbg_unwrap() + .try_into_any() + .dbg_unwrap(); + + approvals.insert(approver); + + visit_seq!(executor.visit_set_account_key_value(&SetKeyValue::account( + multisig_account.clone(), + approvals_key(&instructions_hash), + Json::new(&approvals), + ))); + + let is_authenticated = quorum + <= signatories + .into_iter() + .filter(|(id, _)| approvals.contains(id)) + .map(|(_, weight)| u16::from(weight)) + .sum(); + + let is_expired = proposed_at_ms.saturating_add(transaction_ttl_ms) < now_ms; + + if is_authenticated || is_expired { + // Cleanup the transaction entry + visit_seq!( + executor.visit_remove_account_key_value(&RemoveKeyValue::account( + multisig_account.clone(), + approvals_key(&instructions_hash), + )) + ); + + visit_seq!( + executor.visit_remove_account_key_value(&RemoveKeyValue::account( + multisig_account.clone(), + proposed_at_ms_key(&instructions_hash), + )) + ); + + visit_seq!( + executor.visit_remove_account_key_value(&RemoveKeyValue::account( + multisig_account.clone(), + instructions_key(&instructions_hash), + )) + ); + + if is_expired { + // TODO Notify that the proposal has expired, while returning Ok for the entry deletion to take effect + } else { + // Validate and execute the authenticated multisig transaction + for instruction in instructions { + visit_seq!(executor.visit_instruction(&instruction)); + } + } + } + + Ok(()) + } +} diff --git a/crates/iroha_executor/src/default.rs b/crates/iroha_executor/src/default/mod.rs similarity index 93% rename from crates/iroha_executor/src/default.rs rename to crates/iroha_executor/src/default/mod.rs index 19fe09bb666..7a8479a8ee3 100644 --- a/crates/iroha_executor/src/default.rs +++ b/crates/iroha_executor/src/default/mod.rs @@ -17,13 +17,13 @@ pub use asset_definition::{ visit_set_asset_definition_key_value, visit_transfer_asset_definition, visit_unregister_asset_definition, }; -pub use custom::visit_custom; pub use domain::{ visit_register_domain, visit_remove_domain_key_value, visit_set_domain_key_value, visit_transfer_domain, visit_unregister_domain, }; pub use executor::visit_upgrade; use iroha_smart_contract::data_model::{prelude::*, visit::Visit}; +pub use isi::visit_custom_instruction; pub use log::visit_log; pub use parameter::visit_set_parameter; pub use peer::{visit_register_peer, visit_unregister_peer}; @@ -44,6 +44,8 @@ use crate::{ Execute, }; +pub mod isi; + // NOTE: If any new `visit_..` functions are introduced in this module, one should // not forget to update the default executor boilerplate too, specifically the // `iroha_executor::derive::default::impl_derive_visit` function @@ -117,7 +119,7 @@ pub fn visit_instruction(executor: &mut V, isi: &In executor.visit_upgrade(isi); } InstructionBox::Custom(isi) => { - executor.visit_custom(isi); + executor.visit_custom_instruction(isi); } } } @@ -368,9 +370,7 @@ pub mod domain { AnyPermission::CanRegisterTrigger(permission) => { permission.authority.domain() == domain_id } - AnyPermission::CanRegisterAnyTrigger(_) - | AnyPermission::CanUnregisterAnyTrigger(_) - | AnyPermission::CanUnregisterTrigger(_) + AnyPermission::CanUnregisterTrigger(_) | AnyPermission::CanExecuteTrigger(_) | AnyPermission::CanModifyTrigger(_) | AnyPermission::CanModifyTriggerMetadata(_) @@ -548,9 +548,7 @@ pub mod account { AnyPermission::CanBurnAsset(permission) => permission.asset.account() == account_id, AnyPermission::CanTransferAsset(permission) => permission.asset.account() == account_id, AnyPermission::CanRegisterTrigger(permission) => permission.authority == *account_id, - AnyPermission::CanRegisterAnyTrigger(_) - | AnyPermission::CanUnregisterAnyTrigger(_) - | AnyPermission::CanUnregisterTrigger(_) + AnyPermission::CanUnregisterTrigger(_) | AnyPermission::CanExecuteTrigger(_) | AnyPermission::CanModifyTrigger(_) | AnyPermission::CanModifyTriggerMetadata(_) @@ -816,8 +814,6 @@ pub mod asset_definition { AnyPermission::CanUnregisterAccount(_) | AnyPermission::CanRegisterAsset(_) | AnyPermission::CanModifyAccountMetadata(_) - | AnyPermission::CanRegisterAnyTrigger(_) - | AnyPermission::CanUnregisterAnyTrigger(_) | AnyPermission::CanRegisterTrigger(_) | AnyPermission::CanUnregisterTrigger(_) | AnyPermission::CanExecuteTrigger(_) @@ -1167,7 +1163,7 @@ pub mod parameter { } pub mod role { - use iroha_executor_data_model::permission::{role::CanManageRoles, trigger::CanExecuteTrigger}; + use iroha_executor_data_model::permission::role::CanManageRoles; use iroha_smart_contract::{data_model::role::Role, Iroha}; use super::*; @@ -1235,40 +1231,49 @@ pub mod role { isi: &Register, ) { let role = isi.object(); + let grant_role = &Grant::account_role(role.id().clone(), role.grant_to().clone()); let mut new_role = Role::new(role.id().clone(), role.grant_to().clone()); // Exception for multisig roles - let mut is_multisig_role = false; - if let Some(tail) = role - .id() - .name() - .as_ref() - .strip_prefix("multisig_signatory_") { - let Ok(account_id) = tail.replacen('_', "@", 1).parse::() else { - deny!(executor, "Violates multisig role format") - }; - if crate::permission::domain::is_domain_owner( - account_id.domain(), - &executor.context().authority, - executor.host(), - ) - .unwrap_or_default() - { - // Bind this role to this permission here, regardless of the given contains - let permission = CanExecuteTrigger { - trigger: format!( - "multisig_transactions_{}_{}", - account_id.signatory(), - account_id.domain() + use crate::permission::domain::is_domain_owner; + + const DELIMITER: char = '/'; + const MULTISIG_SIGNATORY: &str = "MULTISIG_SIGNATORY"; + + fn multisig_account_from(role: &RoleId) -> Option { + role.name() + .as_ref() + .strip_prefix(MULTISIG_SIGNATORY)? + .rsplit_once(DELIMITER) + .and_then(|(init, last)| { + format!("{last}@{}", init.trim_matches(DELIMITER)) + .parse() + .ok() + }) + } + + if role.id().name().as_ref().starts_with(MULTISIG_SIGNATORY) { + if let Some(multisig_account) = multisig_account_from(role.id()) { + if is_domain_owner( + multisig_account.domain(), + &executor.context().authority, + executor.host(), ) - .parse() - .unwrap(), - }; - new_role = new_role.add_permission(permission); - is_multisig_role = true; - } else { - deny!(executor, "Can't register multisig role") + .unwrap_or_default() + { + let isi = &Register::role(new_role); + if let Err(err) = executor.host().submit(isi) { + deny!(executor, err); + } + execute!(executor, grant_role); + } + deny!( + executor, + "only the domain owner can register multisig roles" + ) + } + deny!(executor, "violates multisig role name format") } } @@ -1298,12 +1303,10 @@ pub mod role { if executor.context().curr_block.is_genesis() || CanManageRoles.is_owned_by(&executor.context().authority, executor.host()) - || is_multisig_role { - let grant_role = &Grant::account_role(role.id().clone(), role.grant_to().clone()); let isi = &Register::role(new_role); if let Err(err) = executor.host().submit(isi) { - executor.deny(err); + deny!(executor, err); } execute!(executor, grant_role); @@ -1357,8 +1360,8 @@ pub mod role { pub mod trigger { use iroha_executor_data_model::permission::trigger::{ - CanExecuteTrigger, CanModifyTrigger, CanModifyTriggerMetadata, CanRegisterAnyTrigger, - CanRegisterTrigger, CanUnregisterAnyTrigger, CanUnregisterTrigger, + CanExecuteTrigger, CanModifyTrigger, CanModifyTriggerMetadata, CanRegisterTrigger, + CanUnregisterTrigger, }; use iroha_smart_contract::data_model::trigger::Trigger; @@ -1374,37 +1377,6 @@ pub mod trigger { let trigger = isi.object(); let is_genesis = executor.context().curr_block.is_genesis(); - let trigger_name = trigger.id().name().as_ref(); - - #[expect(clippy::option_if_let_else)] // clippy suggestion spoils readability - let naming_is_ok = if let Some(tail) = trigger_name.strip_prefix("multisig_accounts_") { - let system_account: AccountId = - // predefined in `GenesisBuilder::default` - "ed0120D8B64D62FD8E09B9F29FE04D9C63E312EFB1CB29F1BF6AF00EBC263007AE75F7@system" - .parse() - .unwrap(); - tail.parse::().is_ok() - && (is_genesis || executor.context().authority == system_account) - } else if let Some(tail) = trigger_name.strip_prefix("multisig_transactions_") { - tail.replacen('_', "@", 1) - .parse::() - .ok() - .and_then(|account_id| { - is_domain_owner( - account_id.domain(), - &executor.context().authority, - executor.host(), - ) - .ok() - }) - .unwrap_or_default() - } else { - true - }; - if !naming_is_ok { - deny!(executor, "Violates trigger naming restrictions"); - } - if is_genesis || { match is_domain_owner( @@ -1423,7 +1395,6 @@ pub mod trigger { can_register_user_trigger_token .is_owned_by(&executor.context().authority, executor.host()) } - || CanRegisterAnyTrigger.is_owned_by(&executor.context().authority, executor.host()) { execute!(executor, isi) } @@ -1448,7 +1419,6 @@ pub mod trigger { can_unregister_user_trigger_token .is_owned_by(&executor.context().authority, executor.host()) } - || CanUnregisterAnyTrigger.is_owned_by(&executor.context().authority, executor.host()) { let mut err = None; for (owner_id, permission) in accounts_permissions(executor.host()) { @@ -1557,20 +1527,6 @@ pub mod trigger { if can_execute_trigger_token.is_owned_by(authority, executor.host()) { execute!(executor, isi); } - // Any account in domain can call multisig accounts registry to register any multisig account in the domain - // TODO Restrict access to the multisig signatories? - // TODO Impose proposal and approval process? - if trigger_id - .name() - .as_ref() - .strip_prefix("multisig_accounts_") - .and_then(|s| s.parse::().ok()) - .map_or(false, |registry_domain| { - *authority.domain() == registry_domain - }) - { - execute!(executor, isi); - } deny!(executor, "Can't execute trigger owned by another account"); } @@ -1644,9 +1600,7 @@ pub mod trigger { AnyPermission::CanModifyTriggerMetadata(permission) => { &permission.trigger == trigger_id } - AnyPermission::CanRegisterAnyTrigger(_) - | AnyPermission::CanUnregisterAnyTrigger(_) - | AnyPermission::CanRegisterTrigger(_) + AnyPermission::CanRegisterTrigger(_) | AnyPermission::CanManagePeers(_) | AnyPermission::CanRegisterDomain(_) | AnyPermission::CanUnregisterDomain(_) @@ -1745,14 +1699,3 @@ pub mod log { execute!(executor, isi) } } - -pub mod custom { - use super::*; - - pub fn visit_custom(executor: &mut V, _isi: &CustomInstruction) { - deny!( - executor, - "Custom instructions should be handled in custom executor" - ) - } -} diff --git a/crates/iroha_executor/src/lib.rs b/crates/iroha_executor/src/lib.rs index a6146e50b82..bce5b1cbe12 100644 --- a/crates/iroha_executor/src/lib.rs +++ b/crates/iroha_executor/src/lib.rs @@ -294,6 +294,10 @@ pub trait Execute { /// Represents the current state of the world fn context(&self) -> &prelude::Context; + /// Mutable context for e.g. switching to another authority after validation before execution. + /// Note that mutations are persistent to the instance unless reset + fn context_mut(&mut self) -> &mut prelude::Context; + /// Executor verdict. fn verdict(&self) -> &Result; diff --git a/crates/iroha_executor/src/permission.rs b/crates/iroha_executor/src/permission.rs index f39cc199d71..7460b5e0df4 100644 --- a/crates/iroha_executor/src/permission.rs +++ b/crates/iroha_executor/src/permission.rs @@ -116,8 +116,6 @@ declare_permissions! { iroha_executor_data_model::permission::parameter::{CanSetParameters}, iroha_executor_data_model::permission::role::{CanManageRoles}, - iroha_executor_data_model::permission::trigger::{CanRegisterAnyTrigger}, - iroha_executor_data_model::permission::trigger::{CanUnregisterAnyTrigger}, iroha_executor_data_model::permission::trigger::{CanRegisterTrigger}, iroha_executor_data_model::permission::trigger::{CanUnregisterTrigger}, iroha_executor_data_model::permission::trigger::{CanModifyTrigger}, @@ -755,8 +753,8 @@ pub mod account { pub mod trigger { //! Module with pass conditions for trigger related tokens use iroha_executor_data_model::permission::trigger::{ - CanExecuteTrigger, CanModifyTrigger, CanModifyTriggerMetadata, CanRegisterAnyTrigger, - CanRegisterTrigger, CanUnregisterAnyTrigger, CanUnregisterTrigger, + CanExecuteTrigger, CanModifyTrigger, CanModifyTriggerMetadata, CanRegisterTrigger, + CanUnregisterTrigger, }; use super::*; @@ -820,34 +818,6 @@ pub mod trigger { } } - impl ValidateGrantRevoke for CanRegisterAnyTrigger { - fn validate_grant(&self, authority: &AccountId, context: &Context, host: &Iroha) -> Result { - OnlyGenesis::from(self).validate(authority, host, context) - } - fn validate_revoke( - &self, - authority: &AccountId, - context: &Context, - host: &Iroha, - ) -> Result { - OnlyGenesis::from(self).validate(authority, host, context) - } - } - - impl ValidateGrantRevoke for CanUnregisterAnyTrigger { - fn validate_grant(&self, authority: &AccountId, context: &Context, host: &Iroha) -> Result { - OnlyGenesis::from(self).validate(authority, host, context) - } - fn validate_revoke( - &self, - authority: &AccountId, - context: &Context, - host: &Iroha, - ) -> Result { - OnlyGenesis::from(self).validate(authority, host, context) - } - } - impl ValidateGrantRevoke for CanRegisterTrigger { fn validate_grant(&self, authority: &AccountId, context: &Context, host: &Iroha) -> Result { super::account::Owner::from(self).validate(authority, host, context) diff --git a/crates/iroha_executor_data_model/Cargo.toml b/crates/iroha_executor_data_model/Cargo.toml index 4627792101d..df1d60ab4da 100644 --- a/crates/iroha_executor_data_model/Cargo.toml +++ b/crates/iroha_executor_data_model/Cargo.toml @@ -16,5 +16,6 @@ iroha_executor_data_model_derive = { path = "../iroha_executor_data_model_derive iroha_data_model.workspace = true iroha_schema.workspace = true +derive_more = { workspace = true, features = ["constructor", "from"] } serde.workspace = true serde_json.workspace = true diff --git a/crates/iroha_executor_data_model/src/isi.rs b/crates/iroha_executor_data_model/src/isi.rs new file mode 100644 index 00000000000..255df1df20e --- /dev/null +++ b/crates/iroha_executor_data_model/src/isi.rs @@ -0,0 +1,118 @@ +//! Types for custom instructions + +use alloc::{collections::btree_map::BTreeMap, format, string::String, vec::Vec}; + +use derive_more::{Constructor, From}; +use iroha_data_model::{ + isi::{CustomInstruction, Instruction, InstructionBox}, + prelude::{Json, *}, +}; +use iroha_schema::IntoSchema; +use serde::{Deserialize, Serialize}; + +use super::*; + +macro_rules! impl_custom_instruction { + ($box:ty, $($instruction:ty)|+) => { + impl Instruction for $box {} + + impl From<$box> for InstructionBox { + fn from(value: $box) -> Self { + Self::Custom(value.into()) + } + } + + impl From<$box> for CustomInstruction { + fn from(value: $box) -> Self { + let payload = serde_json::to_value(&value) + .expect(concat!("INTERNAL BUG: Couldn't serialize ", stringify!($box))); + + Self::new(payload) + } + } + + impl TryFrom<&Json> for $box { + type Error = serde_json::Error; + + fn try_from(payload: &Json) -> serde_json::Result { + serde_json::from_str::(payload.as_ref()) + } + } $( + + impl Instruction for $instruction {} + + impl From<$instruction> for InstructionBox { + fn from(value: $instruction) -> Self { + Self::Custom(<$box>::from(value).into()) + } + })+ + }; +} + +/// Types for multisig instructions +pub mod multisig { + use core::num::{NonZeroU16, NonZeroU64}; + + use super::*; + + /// Multisig-related instructions + #[derive(Debug, Clone, Serialize, Deserialize, IntoSchema, From)] + pub enum MultisigInstructionBox { + /// Register a multisig account, which is a prerequisite of multisig transactions + Register(MultisigRegister), + /// Propose a multisig transaction and initialize approvals with the proposer's one + Propose(MultisigPropose), + /// Approve a certain multisig transaction + Approve(MultisigApprove), + } + + /// Register a multisig account, which is a prerequisite of multisig transactions + #[derive(Debug, Clone, Serialize, Deserialize, IntoSchema, Constructor)] + pub struct MultisigRegister { + /// Multisig account to be registered + ///
+ /// + /// Any corresponding private key allows the owner to manipulate this account as a ordinary personal account + /// + ///
+ // FIXME #5022 prevent multisig monopoly + // FIXME #5022 stop accepting user input: otherwise, after #4426 pre-registration account will be hijacked as a multisig account + pub account: AccountId, + /// List of signatories and their relative weights of responsibility for the multisig account + pub signatories: BTreeMap, + /// Threshold of total weight at which the multisig account is considered authenticated + pub quorum: NonZeroU16, + /// Multisig transaction time-to-live in milliseconds based on block timestamps. Defaults to [`DEFAULT_MULTISIG_TTL_MS`] + pub transaction_ttl_ms: NonZeroU64, + } + + /// Relative weight of responsibility for the multisig account. + /// 0 is allowed for observers who don't join governance + type Weight = u8; + + /// Default multisig transaction time-to-live in milliseconds based on block timestamps + pub const DEFAULT_MULTISIG_TTL_MS: u64 = 60 * 60 * 1_000; // 1 hour + + /// Propose a multisig transaction and initialize approvals with the proposer's one + #[derive(Debug, Clone, Serialize, Deserialize, IntoSchema, Constructor)] + pub struct MultisigPropose { + /// Multisig account to propose + pub account: AccountId, + /// Proposal contents + pub instructions: Vec, + } + + /// Approve a certain multisig transaction + #[derive(Debug, Clone, Serialize, Deserialize, IntoSchema, Constructor)] + pub struct MultisigApprove { + /// Multisig account to approve + pub account: AccountId, + /// Proposal to approve + pub instructions_hash: HashOf>, + } + + impl_custom_instruction!( + MultisigInstructionBox, + MultisigRegister | MultisigPropose | MultisigApprove + ); +} diff --git a/crates/iroha_executor_data_model/src/lib.rs b/crates/iroha_executor_data_model/src/lib.rs index 7695f7e384b..125a05dcf4e 100644 --- a/crates/iroha_executor_data_model/src/lib.rs +++ b/crates/iroha_executor_data_model/src/lib.rs @@ -4,6 +4,7 @@ extern crate alloc; extern crate self as iroha_executor_data_model; +pub mod isi; pub mod parameter; pub mod permission; diff --git a/crates/iroha_executor_data_model/src/permission.rs b/crates/iroha_executor_data_model/src/permission.rs index dc950a197bb..27778496268 100644 --- a/crates/iroha_executor_data_model/src/permission.rs +++ b/crates/iroha_executor_data_model/src/permission.rs @@ -178,16 +178,6 @@ pub mod asset { pub mod trigger { use super::*; - permission! { - #[derive(Copy)] - pub struct CanRegisterAnyTrigger; - } - - permission! { - #[derive(Copy)] - pub struct CanUnregisterAnyTrigger; - } - permission! { pub struct CanRegisterTrigger { pub authority: AccountId, diff --git a/crates/iroha_executor_derive/src/default.rs b/crates/iroha_executor_derive/src/default.rs index ca9e0abc80a..03c52f33998 100644 --- a/crates/iroha_executor_derive/src/default.rs +++ b/crates/iroha_executor_derive/src/default.rs @@ -155,7 +155,7 @@ pub fn impl_derive_visit(emitter: &mut Emitter, input: &syn::DeriveInput) -> Tok "fn visit_set_parameter(operation: &SetParameter)", "fn visit_upgrade(operation: &Upgrade)", "fn visit_log(operation: &Log)", - "fn visit_custom(operation: &CustomInstruction)", + "fn visit_custom_instruction(operation: &CustomInstruction)", ] .into_iter() .map(|item| { @@ -235,6 +235,10 @@ pub fn impl_derive_execute(emitter: &mut Emitter, input: &syn::DeriveInput) -> T &self.context } + fn context_mut(&mut self) -> &mut ::iroha_executor::prelude::Context { + &mut self.context + } + fn verdict(&self) -> &::iroha_executor::prelude::Result { &self.verdict } diff --git a/crates/iroha_genesis/src/lib.rs b/crates/iroha_genesis/src/lib.rs index 7d11ccd9f63..da671bafec9 100644 --- a/crates/iroha_genesis/src/lib.rs +++ b/crates/iroha_genesis/src/lib.rs @@ -12,9 +12,6 @@ use derive_more::Constructor; use eyre::{eyre, Result, WrapErr}; use iroha_crypto::KeyPair; use iroha_data_model::{block::SignedBlock, parameter::Parameter, prelude::*}; -use iroha_executor_data_model::permission::trigger::{ - CanRegisterAnyTrigger, CanUnregisterAnyTrigger, -}; use iroha_schema::IntoSchema; use parity_scale_codec::{Decode, Encode}; use serde::{Deserialize, Serialize}; @@ -22,21 +19,6 @@ use serde::{Deserialize, Serialize}; /// Domain of the genesis account, technically required for the pre-genesis state pub static GENESIS_DOMAIN_ID: LazyLock = LazyLock::new(|| "genesis".parse().unwrap()); -/// Domain of the system account, implicitly registered in the genesis -pub static SYSTEM_DOMAIN_ID: LazyLock = LazyLock::new(|| "system".parse().unwrap()); - -/// The root authority for internal operations, implicitly registered in the genesis -// FIXME #5022 deny external access -// kagami crypto --seed "system" -pub static SYSTEM_ACCOUNT_ID: LazyLock = LazyLock::new(|| { - AccountId::new( - SYSTEM_DOMAIN_ID.clone(), - "ed0120D8B64D62FD8E09B9F29FE04D9C63E312EFB1CB29F1BF6AF00EBC263007AE75F7" - .parse() - .unwrap(), - ) -}); - /// Genesis block. /// /// First transaction must contain single [`Upgrade`] instruction to set executor. @@ -249,37 +231,6 @@ impl GenesisBuilder { } } - /// Entry system entities to serve standard functionality. - pub fn install_libs(self) -> Self { - // Register a trigger that reacts to domain creation (or owner changes) and registers (or replaces) a multisig accounts registry for the domain - let multisig_domains_initializer = GenesisWasmTrigger::new( - "multisig_domains".parse().unwrap(), - GenesisWasmAction::new( - "multisig_domains.wasm", - Repeats::Indefinitely, - SYSTEM_ACCOUNT_ID.clone(), - DomainEventFilter::new() - .for_events(DomainEventSet::Created | DomainEventSet::OwnerChanged), - ), - ); - let instructions = vec![ - Register::domain(Domain::new(SYSTEM_DOMAIN_ID.clone())).into(), - Register::account(Account::new(SYSTEM_ACCOUNT_ID.clone())).into(), - Grant::account_permission(CanRegisterAnyTrigger, SYSTEM_ACCOUNT_ID.clone()).into(), - Grant::account_permission(CanUnregisterAnyTrigger, SYSTEM_ACCOUNT_ID.clone()).into(), - ]; - - Self { - chain: self.chain, - executor: self.executor, - parameters: self.parameters, - instructions, - wasm_dir: self.wasm_dir, - wasm_triggers: vec![multisig_domains_initializer], - topology: self.topology, - } - } - /// Entry a domain registration and transition to [`GenesisDomainBuilder`]. pub fn domain(self, domain_name: Name) -> GenesisDomainBuilder { self.domain_with_metadata(domain_name, Metadata::default()) diff --git a/crates/iroha_kagami/src/genesis/generate.rs b/crates/iroha_kagami/src/genesis/generate.rs index 441e294c8f2..227b7395ec6 100644 --- a/crates/iroha_kagami/src/genesis/generate.rs +++ b/crates/iroha_kagami/src/genesis/generate.rs @@ -9,9 +9,7 @@ use iroha_data_model::{isi::InstructionBox, parameter::Parameters, prelude::*}; use iroha_executor_data_model::permission::{ domain::CanRegisterDomain, parameter::CanSetParameters, }; -use iroha_genesis::{ - GenesisBuilder, GenesisWasmAction, GenesisWasmTrigger, RawGenesisTransaction, GENESIS_DOMAIN_ID, -}; +use iroha_genesis::{GenesisBuilder, RawGenesisTransaction, GENESIS_DOMAIN_ID}; use iroha_test_samples::{gen_account_in, ALICE_ID, BOB_ID, CARPENTER_ID}; use crate::{Outcome, RunArgs}; @@ -66,7 +64,7 @@ impl RunArgs for Args { } = self; let chain = ChainId::from("00000000-0000-0000-0000-000000000000"); - let builder = GenesisBuilder::new(chain, executor, wasm_dir).install_libs(); + let builder = GenesisBuilder::new(chain, executor, wasm_dir); let genesis = match mode.unwrap_or_default() { Mode::Default => generate_default(builder, genesis_public_key), Mode::Synthetic { @@ -151,24 +149,6 @@ pub fn generate_default( builder = builder.append_instruction(isi); } - // Manually register a multisig accounts registry for wonderland whose creation in genesis does not trigger the initializer - let multisig_accounts_registry_for_wonderland = { - let domain_owner = ALICE_ID.clone(); - let registry_id = "multisig_accounts_wonderland".parse::().unwrap(); - - GenesisWasmTrigger::new( - registry_id.clone(), - GenesisWasmAction::new( - "multisig_accounts.wasm", - Repeats::Indefinitely, - domain_owner, - ExecuteTriggerEventFilter::new().for_trigger(registry_id), - ), - ) - }; - - builder = builder.append_wasm_trigger(multisig_accounts_registry_for_wonderland); - Ok(builder.build_raw()) } diff --git a/crates/iroha_schema/src/lib.rs b/crates/iroha_schema/src/lib.rs index 7318842a8d3..45c9e78c5e6 100644 --- a/crates/iroha_schema/src/lib.rs +++ b/crates/iroha_schema/src/lib.rs @@ -16,7 +16,7 @@ use alloc::{ vec::Vec, }; use core::{ - num::{NonZeroU32, NonZeroU64}, + num::{NonZeroU16, NonZeroU32, NonZeroU64}, ops::RangeInclusive, }; @@ -342,7 +342,7 @@ macro_rules! impl_schema_non_zero_int { )*}; } -impl_schema_non_zero_int!(NonZeroU64 => u64, NonZeroU32 => u32); +impl_schema_non_zero_int!(NonZeroU64 => u64, NonZeroU32 => u32, NonZeroU16 => u16); impl TypeId for String { fn id() -> String { diff --git a/crates/iroha_schema_gen/Cargo.toml b/crates/iroha_schema_gen/Cargo.toml index 47822214c03..9fa7345a374 100644 --- a/crates/iroha_schema_gen/Cargo.toml +++ b/crates/iroha_schema_gen/Cargo.toml @@ -14,7 +14,6 @@ workspace = true # TODO: `transparent_api` feature shouldn't be activated/required here iroha_data_model = { workspace = true, features = ["http", "transparent_api"] } iroha_executor_data_model = { workspace = true } -iroha_multisig_data_model = { workspace = true } iroha_primitives = { workspace = true } iroha_genesis = { workspace = true } diff --git a/crates/iroha_schema_gen/src/lib.rs b/crates/iroha_schema_gen/src/lib.rs index 7fd0f8887a6..0c13c559cc4 100644 --- a/crates/iroha_schema_gen/src/lib.rs +++ b/crates/iroha_schema_gen/src/lib.rs @@ -34,8 +34,7 @@ macro_rules! types { /// shall be included recursively. pub fn build_schemas() -> MetaMap { use iroha_data_model::prelude::*; - use iroha_executor_data_model::permission; - use iroha_multisig_data_model as multisig; + use iroha_executor_data_model::{isi::multisig, permission}; macro_rules! schemas { ($($t:ty),* $(,)?) => {{ @@ -85,8 +84,6 @@ pub fn build_schemas() -> MetaMap { permission::asset::CanModifyAssetMetadata, permission::parameter::CanSetParameters, permission::role::CanManageRoles, - permission::trigger::CanRegisterAnyTrigger, - permission::trigger::CanUnregisterAnyTrigger, permission::trigger::CanRegisterTrigger, permission::trigger::CanExecuteTrigger, permission::trigger::CanUnregisterTrigger, @@ -94,9 +91,8 @@ pub fn build_schemas() -> MetaMap { permission::trigger::CanModifyTriggerMetadata, permission::executor::CanUpgradeExecutor, - // Arguments attached to multi-signature operations - multisig::MultisigAccountArgs, - multisig::MultisigTransactionArgs, + // Multi-signature operations + multisig::MultisigInstructionBox, // Genesis file - used by SDKs to generate the genesis block // TODO: IMO it could/should be removed from the schema @@ -287,13 +283,12 @@ types!( MintabilityError, Mintable, Mismatch, - MultisigAccountArgs, - MultisigTransactionArgs, Name, NewAccount, NewAssetDefinition, NewDomain, NewRole, + NonZeroU16, NonZeroU32, NonZeroU64, Numeric, @@ -507,7 +502,7 @@ types!( pub mod complete_data_model { //! Complete set of types participating in the schema - pub use core::num::{NonZeroU32, NonZeroU64}; + pub use core::num::{NonZeroU16, NonZeroU32, NonZeroU64}; pub use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; pub use iroha_crypto::*; @@ -551,7 +546,6 @@ pub mod complete_data_model { Level, }; pub use iroha_genesis::{GenesisWasmAction, GenesisWasmTrigger, WasmPath}; - pub use iroha_multisig_data_model::{MultisigAccountArgs, MultisigTransactionArgs}; pub use iroha_primitives::{const_vec::ConstVec, conststr::ConstString, json::Json}; pub use iroha_schema::Compact; } @@ -621,12 +615,6 @@ mod tests { insert_into_test_map!(iroha_executor_data_model::permission::asset::CanModifyAssetMetadata); insert_into_test_map!(iroha_executor_data_model::permission::parameter::CanSetParameters); insert_into_test_map!(iroha_executor_data_model::permission::role::CanManageRoles); - insert_into_test_map!( - iroha_executor_data_model::permission::trigger::CanRegisterAnyTrigger - ); - insert_into_test_map!( - iroha_executor_data_model::permission::trigger::CanUnregisterAnyTrigger - ); insert_into_test_map!(iroha_executor_data_model::permission::trigger::CanRegisterTrigger); insert_into_test_map!(iroha_executor_data_model::permission::trigger::CanExecuteTrigger); insert_into_test_map!(iroha_executor_data_model::permission::trigger::CanUnregisterTrigger); @@ -636,6 +624,11 @@ mod tests { ); insert_into_test_map!(iroha_executor_data_model::permission::executor::CanUpgradeExecutor); + insert_into_test_map!(iroha_executor_data_model::isi::multisig::MultisigInstructionBox); + insert_into_test_map!(iroha_executor_data_model::isi::multisig::MultisigRegister); + insert_into_test_map!(iroha_executor_data_model::isi::multisig::MultisigPropose); + insert_into_test_map!(iroha_executor_data_model::isi::multisig::MultisigApprove); + map } diff --git a/crates/iroha_test_network/src/lib.rs b/crates/iroha_test_network/src/lib.rs index 05cf094b246..5058c9417d3 100644 --- a/crates/iroha_test_network/src/lib.rs +++ b/crates/iroha_test_network/src/lib.rs @@ -783,7 +783,7 @@ impl NetworkPeer { /// Generated [`PeerId`] pub fn peer_id(&self) -> PeerId { - self.id.id.clone() + self.id.id().clone() } /// Check whether the peer is running diff --git a/data_model/libs/iroha_multisig_data_model/Cargo.toml b/data_model/libs/iroha_multisig_data_model/Cargo.toml deleted file mode 100644 index a104d502956..00000000000 --- a/data_model/libs/iroha_multisig_data_model/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "iroha_multisig_data_model" - -edition.workspace = true -version.workspace = true -authors.workspace = true - -license.workspace = true - -[lints] -workspace = true - -[dependencies] -iroha_data_model.workspace = true -iroha_schema.workspace = true - -parity-scale-codec = { workspace = true, features = ["derive"] } -serde.workspace = true -serde_json.workspace = true diff --git a/data_model/libs/iroha_multisig_data_model/src/lib.rs b/data_model/libs/iroha_multisig_data_model/src/lib.rs deleted file mode 100644 index 6e83490cd62..00000000000 --- a/data_model/libs/iroha_multisig_data_model/src/lib.rs +++ /dev/null @@ -1,74 +0,0 @@ -//! Arguments attached on executing triggers for multisig accounts or transactions - -#![no_std] - -extern crate alloc; - -use alloc::{collections::btree_map::BTreeMap, format, string::String, vec::Vec}; - -use iroha_data_model::prelude::*; -use iroha_schema::IntoSchema; -use parity_scale_codec::{Decode, Encode}; -use serde::{Deserialize, Serialize}; - -/// Arguments to register multisig account -#[derive(Debug, Clone, Decode, Encode, Serialize, Deserialize, IntoSchema)] -pub struct MultisigAccountArgs { - /// Multisig account to be registered - ///
- /// - /// Any corresponding private key allows the owner to manipulate this account as a ordinary personal account - /// - ///
- // FIXME #5022 prevent multisig monopoly - // FIXME #5022 stop accepting user input: otherwise, after #4426 pre-registration account will be hijacked as a multisig account - pub account: PublicKey, - /// List of accounts and their relative weights of responsibility for the multisig - pub signatories: BTreeMap, - /// Threshold of total weight at which the multisig is considered authenticated - pub quorum: u16, - /// Multisig transaction time-to-live in milliseconds based on block timestamps. Defaults to [`DEFAULT_MULTISIG_TTL_MS`] - pub transaction_ttl_ms: u64, -} - -type Weight = u8; - -/// Default multisig transaction time-to-live in milliseconds based on block timestamps -pub const DEFAULT_MULTISIG_TTL_MS: u64 = 60 * 60 * 1_000; // 1 hour - -/// Arguments to propose or approve multisig transaction -#[derive(Debug, Clone, Decode, Encode, Serialize, Deserialize, IntoSchema)] -pub enum MultisigTransactionArgs { - /// Propose instructions and initialize approvals with the proposer's one - Propose(Vec), - /// Approve certain instructions - Approve(HashOf>), -} - -impl From for Json { - fn from(details: MultisigAccountArgs) -> Self { - Json::new(details) - } -} - -impl TryFrom<&Json> for MultisigAccountArgs { - type Error = serde_json::Error; - - fn try_from(payload: &Json) -> serde_json::Result { - serde_json::from_str::(payload.as_ref()) - } -} - -impl From for Json { - fn from(details: MultisigTransactionArgs) -> Self { - Json::new(details) - } -} - -impl TryFrom<&Json> for MultisigTransactionArgs { - type Error = serde_json::Error; - - fn try_from(payload: &Json) -> serde_json::Result { - serde_json::from_str::(payload.as_ref()) - } -} diff --git a/defaults/genesis.json b/defaults/genesis.json index 58e7993996d..a859de841cf 100644 --- a/defaults/genesis.json +++ b/defaults/genesis.json @@ -24,45 +24,6 @@ } }, "instructions": [ - { - "Register": { - "Domain": { - "id": "system", - "logo": null, - "metadata": {} - } - } - }, - { - "Register": { - "Account": { - "id": "ed0120D8B64D62FD8E09B9F29FE04D9C63E312EFB1CB29F1BF6AF00EBC263007AE75F7@system", - "metadata": {} - } - } - }, - { - "Grant": { - "Permission": { - "object": { - "name": "CanRegisterAnyTrigger", - "payload": null - }, - "destination": "ed0120D8B64D62FD8E09B9F29FE04D9C63E312EFB1CB29F1BF6AF00EBC263007AE75F7@system" - } - } - }, - { - "Grant": { - "Permission": { - "object": { - "name": "CanUnregisterAnyTrigger", - "payload": null - }, - "destination": "ed0120D8B64D62FD8E09B9F29FE04D9C63E312EFB1CB29F1BF6AF00EBC263007AE75F7@system" - } - } - }, { "Register": { "Domain": { @@ -191,40 +152,6 @@ } ], "wasm_dir": "libs", - "wasm_triggers": [ - { - "id": "multisig_domains", - "action": { - "executable": "multisig_domains.wasm", - "repeats": "Indefinitely", - "authority": "ed0120D8B64D62FD8E09B9F29FE04D9C63E312EFB1CB29F1BF6AF00EBC263007AE75F7@system", - "filter": { - "Data": { - "Domain": { - "id_matcher": null, - "event_set": [ - "Created", - "OwnerChanged" - ] - } - } - } - } - }, - { - "id": "multisig_accounts_wonderland", - "action": { - "executable": "multisig_accounts.wasm", - "repeats": "Indefinitely", - "authority": "ed0120CE7FA46C9DCE7EA4B125E2E36BDB63EA33073E7590AC92816AE1E861B7048B03@wonderland", - "filter": { - "ExecuteTrigger": { - "trigger_id": "multisig_accounts_wonderland", - "authority": null - } - } - } - } - ], + "wasm_triggers": [], "topology": [] } diff --git a/docs/source/references/schema.json b/docs/source/references/schema.json index 375ab8fcc43..dc3c8ea26b3 100644 --- a/docs/source/references/schema.json +++ b/docs/source/references/schema.json @@ -872,7 +872,6 @@ } ] }, - "CanRegisterAnyTrigger": null, "CanRegisterAsset": { "Struct": [ { @@ -930,7 +929,6 @@ } ] }, - "CanUnregisterAnyTrigger": null, "CanUnregisterAsset": { "Struct": [ { @@ -2601,37 +2599,66 @@ } ] }, - "MultisigAccountArgs": { + "MultisigApprove": { "Struct": [ { "name": "account", - "type": "PublicKey" + "type": "AccountId" }, { - "name": "signatories", - "type": "SortedMap" + "name": "instructions_hash", + "type": "HashOf>" + } + ] + }, + "MultisigInstructionBox": { + "Enum": [ + { + "tag": "Register", + "discriminant": 0, + "type": "MultisigRegister" }, { - "name": "quorum", - "type": "u16" + "tag": "Propose", + "discriminant": 1, + "type": "MultisigPropose" }, { - "name": "transaction_ttl_ms", - "type": "u64" + "tag": "Approve", + "discriminant": 2, + "type": "MultisigApprove" } ] }, - "MultisigTransactionArgs": { - "Enum": [ + "MultisigPropose": { + "Struct": [ { - "tag": "Propose", - "discriminant": 0, + "name": "account", + "type": "AccountId" + }, + { + "name": "instructions", "type": "Vec" + } + ] + }, + "MultisigRegister": { + "Struct": [ + { + "name": "account", + "type": "AccountId" }, { - "tag": "Approve", - "discriminant": 1, - "type": "HashOf>" + "name": "signatories", + "type": "SortedMap" + }, + { + "name": "quorum", + "type": "NonZero" + }, + { + "name": "transaction_ttl_ms", + "type": "NonZero" } ] }, @@ -2700,6 +2727,7 @@ } ] }, + "NonZero": "u16", "NonZero": "u32", "NonZero": "u64", "Numeric": { diff --git a/scripts/build_wasm.sh b/scripts/build_wasm.sh index 8e28b719594..da0ee864647 100755 --- a/scripts/build_wasm.sh +++ b/scripts/build_wasm.sh @@ -10,9 +10,6 @@ build() { "libs") NAMES=( # order by dependency - "multisig_transactions" - "multisig_accounts" - "multisig_domains" "default_executor" ) ;; diff --git a/scripts/tests/instructions.json b/scripts/tests/instructions.json index 5385f812f03..a7dc30cfffb 100644 --- a/scripts/tests/instructions.json +++ b/scripts/tests/instructions.json @@ -3,7 +3,7 @@ "SetKeyValue": { "Account": { "object": "ed01201F89368A4F322263C6F1AEF156759A83FB1AD7D93BAA66BFDFA973ACBADA462F@wonderland", - "key": "key", + "key": "success_marker", "value": "congratulations" } } diff --git a/scripts/tests/multisig.recursion.sh b/scripts/tests/multisig.recursion.sh index cc69c33723e..1561ca2cf3e 100644 --- a/scripts/tests/multisig.recursion.sh +++ b/scripts/tests/multisig.recursion.sh @@ -68,25 +68,14 @@ INSTRUCTIONS="../scripts/tests/instructions.json" propose_stdout=($(cat $INSTRUCTIONS | ./iroha --config "client.0.toml" multisig propose --account $MSA_012345)) INSTRUCTIONS_HASH=${propose_stdout[0]} -# ticks as many times as the multisig recursion -TICK="../scripts/tests/tick.json" -for i in $(seq 0 1); do - cat $TICK | ./iroha json transaction -done - # check that one of the leaf signatories is involved LIST=$(./iroha --config "client.5.toml" multisig list all) echo "$LIST" | grep $INSTRUCTIONS_HASH # approve the multisig transaction -HASH_TO_12345=$(echo "$LIST" | grep -A1 "multisig_transactions" | sed 's/_/@/g' | grep -A1 $MSA_345 | tail -n 1 | tr -d '"') +HASH_TO_12345=$(echo "$LIST" | grep -A1 $MSA_345 | tail -n 1 | tr -d '"') ./iroha --config "client.5.toml" multisig approve --account $MSA_345 --instructions-hash $HASH_TO_12345 -# ticks as many times as the multisig recursion -for i in $(seq 0 1); do - cat $TICK | ./iroha json transaction -done - # check that the multisig transaction is executed ./iroha account list all | grep "congratulations" ! ./iroha --config "client.5.toml" multisig list all | grep $INSTRUCTIONS_HASH diff --git a/scripts/tests/multisig.sh b/scripts/tests/multisig.sh index 272e52b0cfb..c3bfa298d86 100644 --- a/scripts/tests/multisig.sh +++ b/scripts/tests/multisig.sh @@ -38,12 +38,12 @@ for signatory in ${SIGNATORIES[@]}; do ./iroha account register --id $signatory done -# register a multisig account +# register a multisig account by the domain owner MULTISIG_ACCOUNT=$(gen_account_id "msa") WEIGHTS=($(yes 1 | head -n $N_SIGNATORIES)) # equal votes QUORUM=$N_SIGNATORIES # unanimous TRANSACTION_TTL="1y 6M 2w 3d 12h 30m 30s 500ms" -./iroha --config "client.1.toml" multisig register --account $MULTISIG_ACCOUNT --signatories ${SIGNATORIES[*]} --weights ${WEIGHTS[*]} --quorum $QUORUM --transaction-ttl "$TRANSACTION_TTL" +./iroha --config "client.toml" multisig register --account $MULTISIG_ACCOUNT --signatories ${SIGNATORIES[*]} --weights ${WEIGHTS[*]} --quorum $QUORUM --transaction-ttl "$TRANSACTION_TTL" # propose a multisig transaction INSTRUCTIONS="../scripts/tests/instructions.json" diff --git a/wasm/Cargo.toml b/wasm/Cargo.toml index ad54dafcb30..60db2044600 100644 --- a/wasm/Cargo.toml +++ b/wasm/Cargo.toml @@ -30,7 +30,6 @@ iroha_executor = { version = "=2.0.0-rc.1.0", path = "../crates/iroha_executor", iroha_schema = { version = "=2.0.0-rc.1.0", path = "../crates/iroha_schema" } iroha_data_model = { version = "=2.0.0-rc.1.0", path = "../crates/iroha_data_model", default-features = false } -iroha_multisig_data_model = { version = "=2.0.0-rc.1.0", path = "../data_model/libs/iroha_multisig_data_model" } iroha_executor_data_model = { version = "=2.0.0-rc.1.0", path = "../crates/iroha_executor_data_model" } mint_rose_trigger_data_model = { path = "../data_model/samples/mint_rose_trigger_data_model" } executor_custom_data_model = { path = "../data_model/samples/executor_custom_data_model" } diff --git a/wasm/libs/multisig_accounts/Cargo.toml b/wasm/libs/multisig_accounts/Cargo.toml deleted file mode 100644 index d8162aa48c9..00000000000 --- a/wasm/libs/multisig_accounts/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "multisig_accounts" - -edition.workspace = true -version.workspace = true -authors.workspace = true - -license.workspace = true - -[lib] -crate-type = ['cdylib'] - -[dependencies] -iroha_trigger.workspace = true -iroha_executor_data_model.workspace = true -iroha_multisig_data_model.workspace = true - -panic-halt.workspace = true -dlmalloc.workspace = true - -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true, default-features = false } diff --git a/wasm/libs/multisig_accounts/src/lib.rs b/wasm/libs/multisig_accounts/src/lib.rs deleted file mode 100644 index 24401b85916..00000000000 --- a/wasm/libs/multisig_accounts/src/lib.rs +++ /dev/null @@ -1,150 +0,0 @@ -//! Trigger given per domain to control multi-signature accounts and corresponding triggers - -#![no_std] - -extern crate alloc; -#[cfg(not(test))] -extern crate panic_halt; - -use alloc::format; - -use dlmalloc::GlobalDlmalloc; -use iroha_executor_data_model::permission::trigger::CanExecuteTrigger; -use iroha_multisig_data_model::MultisigAccountArgs; -use iroha_trigger::prelude::*; - -#[global_allocator] -static ALLOC: GlobalDlmalloc = GlobalDlmalloc; - -// Binary containing common logic to each multisig account for handling multisig transactions -const MULTISIG_TRANSACTIONS_WASM: &[u8] = core::include_bytes!(concat!( - core::env!("CARGO_MANIFEST_DIR"), - "/../../target/prebuilt/libs/multisig_transactions.wasm" -)); - -#[iroha_trigger::main] -fn main(host: Iroha, context: Context) { - let EventBox::ExecuteTrigger(event) = context.event else { - dbg_panic!("trigger misused: must be triggered only by a call"); - }; - let args: MultisigAccountArgs = event - .args() - .try_into_any() - .dbg_expect("args should be for a multisig account"); - let domain_id = context - .id - .name() - .as_ref() - .strip_prefix("multisig_accounts_") - .and_then(|s| s.parse::().ok()) - .dbg_unwrap(); - let account_id = AccountId::new(domain_id, args.account); - - host.submit(&Register::account(Account::new(account_id.clone()))) - .dbg_expect("accounts registry should successfully register a multisig account"); - - let multisig_transactions_registry_id: TriggerId = format!( - "multisig_transactions_{}_{}", - account_id.signatory(), - account_id.domain() - ) - .parse() - .dbg_unwrap(); - - let multisig_transactions_registry = Trigger::new( - multisig_transactions_registry_id.clone(), - Action::new( - WasmSmartContract::from_compiled(MULTISIG_TRANSACTIONS_WASM.to_vec()), - Repeats::Indefinitely, - account_id.clone(), - ExecuteTriggerEventFilter::new().for_trigger(multisig_transactions_registry_id.clone()), - ), - ); - - host.submit(&Register::trigger(multisig_transactions_registry)) - .dbg_expect("accounts registry should successfully register a transactions registry"); - - host.submit(&SetKeyValue::trigger( - multisig_transactions_registry_id.clone(), - "signatories".parse().unwrap(), - Json::new(&args.signatories), - )) - .dbg_unwrap(); - - host.submit(&SetKeyValue::trigger( - multisig_transactions_registry_id.clone(), - "quorum".parse().unwrap(), - Json::new(&args.quorum), - )) - .dbg_unwrap(); - - host.submit(&SetKeyValue::trigger( - multisig_transactions_registry_id.clone(), - "transaction_ttl_ms".parse().unwrap(), - Json::new(&args.transaction_ttl_ms), - )) - .dbg_unwrap(); - - let role_id: RoleId = format!( - "multisig_signatory_{}_{}", - account_id.signatory(), - account_id.domain() - ) - .parse() - .dbg_unwrap(); - - host.submit(&Register::role( - // Temporarily grant a multisig role to the trigger authority to delegate the role to the signatories - Role::new(role_id.clone(), context.authority.clone()), - )) - .dbg_expect("accounts registry should successfully register a multisig role"); - - for signatory in args.signatories.keys().cloned() { - let is_multisig_again = { - let sub_role_id: RoleId = format!( - "multisig_signatory_{}_{}", - signatory.signatory(), - signatory.domain() - ) - .parse() - .dbg_unwrap(); - - host.query(FindRoleIds) - .filter_with(|role_id| role_id.eq(sub_role_id)) - .execute_single_opt() - .dbg_unwrap() - .is_some() - }; - - if is_multisig_again { - // Allow the transactions registry to write to the sub registry - let sub_registry_id: TriggerId = format!( - "multisig_transactions_{}_{}", - signatory.signatory(), - signatory.domain() - ) - .parse() - .dbg_unwrap(); - - host.submit(&Grant::account_permission( - CanExecuteTrigger { - trigger: sub_registry_id, - }, - account_id.clone(), - )) - .dbg_expect( - "accounts registry should successfully grant permission to the multisig account", - ); - } - - host.submit(&Grant::account_role(role_id.clone(), signatory)) - .dbg_expect( - "accounts registry should successfully grant the multisig role to signatories", - ); - } - - host.submit(&Revoke::account_role(role_id.clone(), context.authority)) - .dbg_expect( - "accounts registry should successfully revoke the multisig role from the trigger authority", - ); -} diff --git a/wasm/libs/multisig_domains/Cargo.toml b/wasm/libs/multisig_domains/Cargo.toml deleted file mode 100644 index efbab17b923..00000000000 --- a/wasm/libs/multisig_domains/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "multisig_domains" - -edition.workspace = true -version.workspace = true -authors.workspace = true - -license.workspace = true - -[lib] -crate-type = ['cdylib'] - -[dependencies] -iroha_trigger.workspace = true -iroha_executor_data_model.workspace = true - -panic-halt.workspace = true -dlmalloc.workspace = true - -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true, default-features = false } diff --git a/wasm/libs/multisig_domains/src/lib.rs b/wasm/libs/multisig_domains/src/lib.rs deleted file mode 100644 index 9b93d096d7f..00000000000 --- a/wasm/libs/multisig_domains/src/lib.rs +++ /dev/null @@ -1,77 +0,0 @@ -//! Trigger of world-level authority to enable multisig functionality for domains - -#![no_std] - -extern crate alloc; -#[cfg(not(test))] -extern crate panic_halt; - -use alloc::format; - -use dlmalloc::GlobalDlmalloc; -use iroha_trigger::prelude::*; - -#[global_allocator] -static ALLOC: GlobalDlmalloc = GlobalDlmalloc; - -// Binary containing common logic to each domain for handling multisig accounts -const MULTISIG_ACCOUNTS_WASM: &[u8] = core::include_bytes!(concat!( - core::env!("CARGO_MANIFEST_DIR"), - "/../../target/prebuilt/libs/multisig_accounts.wasm" -)); - -#[iroha_trigger::main] -fn main(host: Iroha, context: Context) { - let EventBox::Data(DataEvent::Domain(event)) = context.event else { - dbg_panic!("trigger misused: must be triggered only by a domain event"); - }; - let (domain_id, domain_owner, owner_changed) = match event { - DomainEvent::Created(domain) => (domain.id().clone(), domain.owned_by().clone(), false), - DomainEvent::OwnerChanged(owner_changed) => ( - owner_changed.domain().clone(), - owner_changed.new_owner().clone(), - true, - ), - _ => dbg_panic!( - "trigger misused: must be triggered only when domain created or owner changed" - ), - }; - - let accounts_registry_id: TriggerId = format!("multisig_accounts_{}", domain_id) - .parse() - .dbg_unwrap(); - - let accounts_registry = if owner_changed { - let existing = host - .query(FindTriggers::new()) - .filter_with(|trigger| trigger.id.eq(accounts_registry_id.clone())) - .execute_single() - .dbg_expect("accounts registry should be existing"); - - host.submit(&Unregister::trigger(existing.id().clone())) - .dbg_expect("accounts registry should be successfully unregistered"); - - Trigger::new( - existing.id().clone(), - Action::new( - existing.action().executable().clone(), - existing.action().repeats().clone(), - domain_owner, - existing.action().filter().clone(), - ), - ) - } else { - Trigger::new( - accounts_registry_id.clone(), - Action::new( - WasmSmartContract::from_compiled(MULTISIG_ACCOUNTS_WASM.to_vec()), - Repeats::Indefinitely, - domain_owner, - ExecuteTriggerEventFilter::new().for_trigger(accounts_registry_id.clone()), - ), - ) - }; - - host.submit(&Register::trigger(accounts_registry)) - .dbg_expect("accounts registry should be successfully registered"); -} diff --git a/wasm/libs/multisig_transactions/Cargo.toml b/wasm/libs/multisig_transactions/Cargo.toml deleted file mode 100644 index dd676e46c23..00000000000 --- a/wasm/libs/multisig_transactions/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "multisig_transactions" - -edition.workspace = true -version.workspace = true -authors.workspace = true - -license.workspace = true - -[lib] -crate-type = ['cdylib'] - -[dependencies] -iroha_trigger.workspace = true -iroha_multisig_data_model.workspace = true - -panic-halt.workspace = true -dlmalloc.workspace = true - -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true, default-features = false } diff --git a/wasm/libs/multisig_transactions/src/lib.rs b/wasm/libs/multisig_transactions/src/lib.rs deleted file mode 100644 index d70c7fc870c..00000000000 --- a/wasm/libs/multisig_transactions/src/lib.rs +++ /dev/null @@ -1,228 +0,0 @@ -//! Trigger given per multi-signature account to control multi-signature transactions - -#![no_std] - -extern crate alloc; -#[cfg(not(test))] -extern crate panic_halt; - -use alloc::{ - collections::{btree_map::BTreeMap, btree_set::BTreeSet}, - format, - vec::Vec, -}; - -use dlmalloc::GlobalDlmalloc; -use iroha_multisig_data_model::MultisigTransactionArgs; -use iroha_trigger::prelude::*; - -#[global_allocator] -static ALLOC: GlobalDlmalloc = GlobalDlmalloc; - -#[iroha_trigger::main] -fn main(host: Iroha, context: Context) { - let EventBox::ExecuteTrigger(event) = context.event else { - dbg_panic!("trigger misused: must be triggered only by a call"); - }; - let trigger_id = context.id; - let args: MultisigTransactionArgs = event - .args() - .try_into_any() - .dbg_expect("args should be for a multisig transaction"); - let signatory = event.authority().clone(); - - let instructions_hash = match &args { - MultisigTransactionArgs::Propose(instructions) => HashOf::new(instructions), - MultisigTransactionArgs::Approve(instructions_hash) => *instructions_hash, - }; - let instructions_metadata_key: Name = format!("proposals/{instructions_hash}/instructions") - .parse() - .unwrap(); - let proposed_at_ms_metadata_key: Name = format!("proposals/{instructions_hash}/proposed_at_ms") - .parse() - .unwrap(); - let approvals_metadata_key: Name = format!("proposals/{instructions_hash}/approvals") - .parse() - .unwrap(); - - let signatories: BTreeMap = host - .query_single(FindTriggerMetadata::new( - trigger_id.clone(), - "signatories".parse().unwrap(), - )) - .dbg_unwrap() - .try_into_any() - .dbg_unwrap(); - - // Recursively deploy multisig authentication down to the personal leaf signatories - for account_id in signatories.keys() { - let sub_transactions_registry_id: TriggerId = format!( - "multisig_transactions_{}_{}", - account_id.signatory(), - account_id.domain() - ) - .parse() - .unwrap(); - - if let Ok(_sub_registry) = host - .query(FindTriggers::new()) - .filter_with(|trigger| trigger.id.eq(sub_transactions_registry_id.clone())) - .execute_single() - { - let propose_to_approve_me: InstructionBox = { - let approve_me: InstructionBox = { - let args = MultisigTransactionArgs::Approve(instructions_hash); - ExecuteTrigger::new(trigger_id.clone()) - .with_args(&args) - .into() - }; - let args = MultisigTransactionArgs::Propose([approve_me].to_vec()); - - ExecuteTrigger::new(sub_transactions_registry_id.clone()) - .with_args(&args) - .into() - }; - host.submit(&propose_to_approve_me) - .dbg_expect("should successfully write to sub registry"); - } - } - - let mut block_headers = host.query(FindBlockHeaders).execute().dbg_unwrap(); - let now_ms: u64 = block_headers - .next() - .dbg_unwrap() - .dbg_unwrap() - .creation_time() - .as_millis() - .try_into() - .dbg_unwrap(); - - let (approvals, instructions) = match args { - MultisigTransactionArgs::Propose(instructions) => { - host.query_single(FindTriggerMetadata::new( - trigger_id.clone(), - approvals_metadata_key.clone(), - )) - .expect_err("instructions shouldn't already be proposed"); - - let approvals = BTreeSet::from([signatory.clone()]); - - host.submit(&SetKeyValue::trigger( - trigger_id.clone(), - instructions_metadata_key.clone(), - Json::new(&instructions), - )) - .dbg_unwrap(); - - host.submit(&SetKeyValue::trigger( - trigger_id.clone(), - proposed_at_ms_metadata_key.clone(), - Json::new(&now_ms), - )) - .dbg_unwrap(); - - host.submit(&SetKeyValue::trigger( - trigger_id.clone(), - approvals_metadata_key.clone(), - Json::new(&approvals), - )) - .dbg_unwrap(); - - (approvals, instructions) - } - MultisigTransactionArgs::Approve(_instructions_hash) => { - let mut approvals: BTreeSet = host - .query_single(FindTriggerMetadata::new( - trigger_id.clone(), - approvals_metadata_key.clone(), - )) - .dbg_expect("instructions should be proposed first") - .try_into_any() - .dbg_unwrap(); - - approvals.insert(signatory.clone()); - - host.submit(&SetKeyValue::trigger( - trigger_id.clone(), - approvals_metadata_key.clone(), - Json::new(&approvals), - )) - .dbg_unwrap(); - - let instructions: Vec = host - .query_single(FindTriggerMetadata::new( - trigger_id.clone(), - instructions_metadata_key.clone(), - )) - .dbg_unwrap() - .try_into_any() - .dbg_unwrap(); - - (approvals, instructions) - } - }; - - let quorum: u16 = host - .query_single(FindTriggerMetadata::new( - trigger_id.clone(), - "quorum".parse().unwrap(), - )) - .dbg_unwrap() - .try_into_any() - .dbg_unwrap(); - - let is_authenticated = quorum - <= signatories - .into_iter() - .filter(|(id, _)| approvals.contains(&id)) - .map(|(_, weight)| weight as u16) - .sum(); - - let is_expired = { - let proposed_at_ms: u64 = host - .query_single(FindTriggerMetadata::new( - trigger_id.clone(), - proposed_at_ms_metadata_key.clone(), - )) - .dbg_unwrap() - .try_into_any() - .dbg_unwrap(); - - let transaction_ttl_ms: u64 = host - .query_single(FindTriggerMetadata::new( - trigger_id.clone(), - "transaction_ttl_ms".parse().unwrap(), - )) - .dbg_unwrap() - .try_into_any() - .dbg_unwrap(); - - proposed_at_ms.saturating_add(transaction_ttl_ms) < now_ms - }; - - if is_authenticated || is_expired { - // Cleanup approvals and instructions - host.submit(&RemoveKeyValue::trigger( - trigger_id.clone(), - approvals_metadata_key, - )) - .dbg_unwrap(); - host.submit(&RemoveKeyValue::trigger( - trigger_id.clone(), - proposed_at_ms_metadata_key, - )) - .dbg_unwrap(); - host.submit(&RemoveKeyValue::trigger( - trigger_id.clone(), - instructions_metadata_key, - )) - .dbg_unwrap(); - - if !is_expired { - // Execute instructions proposal which collected enough approvals - for isi in instructions { - host.submit(&isi).dbg_unwrap(); - } - } - } -} diff --git a/wasm/samples/executor_custom_instructions_complex/src/lib.rs b/wasm/samples/executor_custom_instructions_complex/src/lib.rs index f75bc03538c..3d6e3f425e1 100644 --- a/wasm/samples/executor_custom_instructions_complex/src/lib.rs +++ b/wasm/samples/executor_custom_instructions_complex/src/lib.rs @@ -23,14 +23,14 @@ use iroha_executor::{ static ALLOC: GlobalDlmalloc = GlobalDlmalloc; #[derive(Visit, Execute, Entrypoints)] -#[visit(custom(visit_custom))] +#[visit(custom(visit_custom_instruction))] struct Executor { host: Iroha, context: iroha_executor::prelude::Context, verdict: Result, } -fn visit_custom(executor: &mut Executor, isi: &CustomInstruction) { +fn visit_custom_instruction(executor: &mut Executor, isi: &CustomInstruction) { let Ok(isi) = CustomInstructionExpr::try_from(isi.payload()) else { deny!(executor, "Failed to parse custom instruction"); }; diff --git a/wasm/samples/executor_custom_instructions_simple/src/lib.rs b/wasm/samples/executor_custom_instructions_simple/src/lib.rs index 9a5db335edc..1f9203eb862 100644 --- a/wasm/samples/executor_custom_instructions_simple/src/lib.rs +++ b/wasm/samples/executor_custom_instructions_simple/src/lib.rs @@ -16,14 +16,14 @@ use iroha_executor::{data_model::isi::CustomInstruction, prelude::*}; static ALLOC: GlobalDlmalloc = GlobalDlmalloc; #[derive(Visit, Execute, Entrypoints)] -#[visit(custom(visit_custom))] +#[visit(custom(visit_custom_instruction))] struct Executor { host: Iroha, context: Context, verdict: Result, } -fn visit_custom(executor: &mut Executor, isi: &CustomInstruction) { +fn visit_custom_instruction(executor: &mut Executor, isi: &CustomInstruction) { let Ok(isi) = CustomInstructionBox::try_from(isi.payload()) else { deny!(executor, "Failed to parse custom instruction"); };