Skip to content

Commit

Permalink
feat: improve multisig utility and usability
Browse files Browse the repository at this point in the history
BREAKING CHANGES:

- (api-changes) `CanRegisterAnyTrigger` permission
- (config-changes) `defaults/genesis.json` contains the multisig initializer

Major commits:

- feat: support multisig recursion
- feat: introduce multisig quorum and weights
- feat: add multisig subcommand to client CLI
- feat: introduce multisig transaction time-to-live
- feat: predefine multisig world-level trigger in genesis
- feat: allow accounts in domain to register multisig accounts

Signed-off-by: Shunkichi Sato <[email protected]>
  • Loading branch information
s8sato committed Sep 3, 2024
1 parent 02479ce commit a409cca
Show file tree
Hide file tree
Showing 30 changed files with 1,418 additions and 358 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

439 changes: 342 additions & 97 deletions client/tests/integration/multisig.rs

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions client_cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ path = "src/main.rs"
iroha = { workspace = true }
iroha_primitives = { workspace = true }
iroha_config_base = { workspace = true }
executor_custom_data_model = { version = "=2.0.0-rc.1.0", path = "../wasm_samples/executor_custom_data_model" }

thiserror = { workspace = true }
error-stack = { workspace = true, features = ["eyre"] }
Expand Down
226 changes: 225 additions & 1 deletion client_cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ enum Subcommand {
Blocks(blocks::Args),
/// The subcommand related to multi-instructions as Json or Json5
Json(json::Args),
/// The subcommand related to multisig accounts and transactions
#[clap(subcommand)]
Multisig(multisig::Args),
}

/// Context inside which command is executed
Expand Down Expand Up @@ -165,7 +168,7 @@ macro_rules! match_all {
impl RunArgs for Subcommand {
fn run(self, context: &mut dyn RunContext) -> Result<()> {
use Subcommand::*;
match_all!((self, context), { Domain, Account, Asset, Peer, Events, Wasm, Blocks, Json })
match_all!((self, context), { Domain, Account, Asset, Peer, Events, Wasm, Blocks, Json, Multisig })
}
}

Expand Down Expand Up @@ -1197,6 +1200,227 @@ mod json {
}
}
}

mod multisig {
use std::io::{BufReader, Read as _};

use executor_custom_data_model::multisig::{MultisigAccountArgs, MultisigTransactionArgs};

use super::*;

/// Arguments for multisig subcommand
#[derive(Debug, clap::Subcommand)]
pub enum Args {
/// Register a multisig account
Register(Register),
/// Propose a multisig transaction
Propose(Propose),
/// Approve a multisig transaction
Approve(Approve),
/// List pending multisig transactions relevant to you
#[clap(subcommand)]
List(List),
}

impl RunArgs for Args {
fn run(self, context: &mut dyn RunContext) -> Result<()> {
match_all!((self, context), { Args::Register, Args::Propose, Args::Approve, Args::List })
}
}
/// Args to register a multisig account
#[derive(Debug, clap::Args)]
pub struct Register {
/// ID of the multisig account to be registered
#[arg(short, long)]
pub account: AccountId,
/// Signatories of the multisig account
#[arg(short, long, num_args(2..))]
pub signatories: Vec<AccountId>,
/// Relative weights of responsibility of respective signatories
#[arg(short, long, num_args(2..))]
pub weights: Vec<u8>,
/// Threshold of total weight at which the multisig is considered authenticated
#[arg(short, long)]
pub quorum: u16,
/// Time-to-live of multisig transactions made by the multisig account
#[arg(short, long)]
pub transaction_ttl_secs: Option<u32>,
}

impl RunArgs for Register {
fn run(self, context: &mut dyn RunContext) -> Result<()> {
let Self {
account,
signatories,
weights,
quorum,
transaction_ttl_secs,
} = self;
let registry_id: TriggerId = format!("multisig_accounts_{}", account.domain())
.parse()
.unwrap();
let account = Account::new(account);
let signatories = signatories.into_iter().zip(weights).collect();
let args = MultisigAccountArgs {
account,
signatories,
quorum,
transaction_ttl_secs,
};
let register_multisig_account =
iroha::data_model::isi::ExecuteTrigger::new(registry_id).with_args(&args);

submit([register_multisig_account], Metadata::default(), context)
.wrap_err("Failed to register multisig account")
}
}

/// Args to propose a multisig transaction
#[derive(Debug, clap::Args)]
pub struct Propose {
/// Multisig authority of the multisig transaction
#[arg(short, long)]
pub account: AccountId,
}

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<InstructionBox> = {
let mut reader = BufReader::new(stdin());
let mut raw_content = Vec::new();
reader.read_to_end(&mut raw_content)?;
let string_content = String::from_utf8(raw_content)?;
json5::from_str(&string_content)?
};
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);

submit([propose_multisig_transaction], Metadata::default(), context)
.wrap_err("Failed to propose transaction")
}
}

/// Args to approve a multisig transaction
#[derive(Debug, clap::Args)]
pub struct Approve {
/// Multisig authority of the multisig transaction
#[arg(short, long)]
pub account: AccountId,
/// Instructions to approve
#[arg(short, long)]
pub instructions_hash: iroha::crypto::Hash,
}

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 instructions_hash = HashOf::from_untyped_unchecked(instructions_hash);
let args = MultisigTransactionArgs::Approve(instructions_hash);
let approve_multisig_transaction =
iroha::data_model::isi::ExecuteTrigger::new(registry_id).with_args(&args);

submit([approve_multisig_transaction], Metadata::default(), context)
.wrap_err("Failed to approve transaction")
}
}

/// List pending multisig transactions relevant to you
#[derive(clap::Subcommand, Debug, Clone)]
pub enum List {
/// All pending multisig transactions relevant to you
All,
}

impl RunArgs for List {
fn run(self, context: &mut dyn RunContext) -> Result<()> {
let client = context.client_from_config();
let me = client.account.clone();

trace_back_from(me, &client, context)
}
}

/// Recursively trace back to the root multisig account
fn trace_back_from(
account: AccountId,
client: &Client,
context: &mut dyn RunContext,
) -> Result<()> {
let Ok(multisig_roles) = client
.query(FindRolesByAccountId::new(account))
.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()
.replace('_', "@")
.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_single(FindTriggerById::new(transactions_registry_id))?;
let proposal_kvs = transactions_registry
.action()
.metadata()
.iter()
.filter(|kv| kv.0.as_ref().starts_with("proposals"));

proposal_kvs.fold("", |acc, (k, v)| {
let mut path = k.as_ref().split('/');
let hash = path.nth(1).unwrap();

if acc != hash {
context.print_data(&hash).unwrap();
}
path.for_each(|seg| context.print_data(&seg).unwrap());
context.print_data(&v).unwrap();

hash
});
}

Ok(())
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
Expand Down
Binary file modified defaults/executor.wasm
Binary file not shown.
57 changes: 57 additions & 0 deletions defaults/genesis.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/source/references/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,7 @@
}
]
},
"CanRegisterAnyTrigger": null,
"CanRegisterAssetDefinitionInDomain": {
"Struct": [
{
Expand Down
3 changes: 0 additions & 3 deletions hooks/pre-commit.sample
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@
set -e
# format checks
cargo fmt --all -- --check
cd ./wasm_samples/default_executor
cargo fmt --all -- --check
cd -
cd ./wasm_samples
cargo fmt --all -- --check
cd -
Expand Down
4 changes: 4 additions & 0 deletions schema/gen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ pub fn build_schemas() -> MetaMap {
permission::asset::CanRemoveKeyValueInUserAsset,
permission::parameter::CanSetParameters,
permission::role::CanUnregisterAnyRole,
permission::trigger::CanRegisterAnyTrigger,
permission::trigger::CanRegisterUserTrigger,
permission::trigger::CanExecuteUserTrigger,
permission::trigger::CanUnregisterUserTrigger,
Expand Down Expand Up @@ -632,6 +633,9 @@ mod tests {
);
insert_into_test_map!(iroha_executor_data_model::permission::parameter::CanSetParameters);
insert_into_test_map!(iroha_executor_data_model::permission::role::CanUnregisterAnyRole);
insert_into_test_map!(
iroha_executor_data_model::permission::trigger::CanRegisterAnyTrigger
);
insert_into_test_map!(
iroha_executor_data_model::permission::trigger::CanRegisterUserTrigger
);
Expand Down
11 changes: 11 additions & 0 deletions scripts/tests/instructions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[
{
"Register": {
"Domain": {
"id": "multiverse",
"logo": null,
"metadata": {}
}
}
}
]
Loading

0 comments on commit a409cca

Please sign in to comment.