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` `CanUnregisterAnyTrigger` permission
- (config-changes) `defaults/genesis.json` assumes `wasm_triggers[*].action.executable` is prebuilt under `wasm_samples/target/prebuilt/`

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 Oct 8, 2024
1 parent 90940a1 commit 84c1ec8
Show file tree
Hide file tree
Showing 35 changed files with 1,858 additions and 428 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.

568 changes: 454 additions & 114 deletions crates/iroha/tests/integration/multisig.rs

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions crates/iroha_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
231 changes: 230 additions & 1 deletion crates/iroha_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,232 @@ 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;
if signatories.len() != 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 account = account.signatory.clone();
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(FindTriggers::new())
.filter_with(|trigger| trigger.id.eq(transactions_registry_id.clone()))
.execute_single()?;
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 super::*;
Expand Down
4 changes: 2 additions & 2 deletions crates/iroha_data_model/src/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -459,9 +459,9 @@ mod candidate {
);
};

if transactions.len() > 4 {
if transactions.len() > 5 {
return Err(
"Genesis block must have 1 to 4 transactions (executor upgrade, initial topology, parameters, other isi)",
"Genesis block must have 1 to 5 transactions (executor upgrade, initial topology, parameters, other instructions, trigger registrations)",
);
}

Expand Down
Loading

0 comments on commit 84c1ec8

Please sign in to comment.