From 6fd46f2c7cb1e4ffbc64f5a7de2f8110649bc6b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Wed, 4 Jan 2023 15:30:06 +0100 Subject: [PATCH 01/11] Full basic support --- aleph-client/src/pallets/multisig.rs | 56 ++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 4 deletions(-) diff --git a/aleph-client/src/pallets/multisig.rs b/aleph-client/src/pallets/multisig.rs index e92bdbe122..7660707c7e 100644 --- a/aleph-client/src/pallets/multisig.rs +++ b/aleph-client/src/pallets/multisig.rs @@ -1,17 +1,32 @@ use primitives::{Balance, BlockNumber}; use crate::{ - api, api::runtime_types, sp_weights::weight_v2::Weight, AccountId, BlockHash, - SignedConnectionApi, TxStatus, + aleph_runtime::RuntimeCall, api, api::runtime_types, sp_weights::weight_v2::Weight, AccountId, + BlockHash, SignedConnectionApi, TxStatus, }; pub type CallHash = [u8; 32]; -pub type Call = Vec; -pub type Timepoint = api::runtime_types::pallet_multisig::Timepoint; +pub type Call = RuntimeCall; +pub type Timepoint = runtime_types::pallet_multisig::Timepoint; pub type Multisig = runtime_types::pallet_multisig::Multisig; #[async_trait::async_trait] pub trait MultisigUserApi { + async fn as_multi_threshold_1( + &self, + other_signatories: Vec, + call: Call, + status: TxStatus, + ) -> anyhow::Result; + async fn as_multi( + &self, + threshold: u16, + other_signatories: Vec, + timepoint: Option, + max_weight: Weight, + call: Call, + status: TxStatus, + ) -> anyhow::Result; async fn approve_as_multi( &self, threshold: u16, @@ -33,6 +48,39 @@ pub trait MultisigUserApi { #[async_trait::async_trait] impl MultisigUserApi for S { + async fn as_multi_threshold_1( + &self, + other_signatories: Vec, + call: Call, + status: TxStatus, + ) -> anyhow::Result { + let tx = api::tx() + .multisig() + .as_multi_threshold_1(other_signatories, call); + + self.send_tx(tx, status).await + } + + async fn as_multi( + &self, + threshold: u16, + other_signatories: Vec, + timepoint: Option, + max_weight: Weight, + call: Call, + status: TxStatus, + ) -> anyhow::Result { + let tx = api::tx().multisig().as_multi( + threshold, + other_signatories, + timepoint, + call, + max_weight, + ); + + self.send_tx(tx, status).await + } + async fn approve_as_multi( &self, threshold: u16, From 46a6ad67a9862f95aaaee17f5db3948a774e80a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Thu, 5 Jan 2023 07:39:10 +0100 Subject: [PATCH 02/11] Api is ready --- aleph-client/src/pallets/multisig.rs | 311 ++++++++++++++++++++++++++- 1 file changed, 303 insertions(+), 8 deletions(-) diff --git a/aleph-client/src/pallets/multisig.rs b/aleph-client/src/pallets/multisig.rs index 7660707c7e..e60cf41401 100644 --- a/aleph-client/src/pallets/multisig.rs +++ b/aleph-client/src/pallets/multisig.rs @@ -1,12 +1,18 @@ +use anyhow::{anyhow, ensure, Result as AnyResult}; +use codec::{Decode, Encode}; use primitives::{Balance, BlockNumber}; +use sp_core::blake2_256; +use sp_runtime::traits::TrailingZeroInput; use crate::{ - aleph_runtime::RuntimeCall, api, api::runtime_types, sp_weights::weight_v2::Weight, AccountId, - BlockHash, SignedConnectionApi, TxStatus, + account_from_keypair, aleph_runtime::RuntimeCall, api, api::runtime_types, + sp_weights::weight_v2::Weight, AccountId, BlockHash, ConnectionApi, SignedConnectionApi, + TxStatus, }; pub type CallHash = [u8; 32]; pub type Call = RuntimeCall; +pub type MultisigThreshold = u16; pub type Timepoint = runtime_types::pallet_multisig::Timepoint; pub type Multisig = runtime_types::pallet_multisig::Multisig; @@ -20,7 +26,7 @@ pub trait MultisigUserApi { ) -> anyhow::Result; async fn as_multi( &self, - threshold: u16, + threshold: MultisigThreshold, other_signatories: Vec, timepoint: Option, max_weight: Weight, @@ -29,7 +35,7 @@ pub trait MultisigUserApi { ) -> anyhow::Result; async fn approve_as_multi( &self, - threshold: u16, + threshold: MultisigThreshold, other_signatories: Vec, timepoint: Option, max_weight: Weight, @@ -38,7 +44,7 @@ pub trait MultisigUserApi { ) -> anyhow::Result; async fn cancel_as_multi( &self, - threshold: u16, + threshold: MultisigThreshold, other_signatories: Vec, timepoint: Timepoint, call_hash: CallHash, @@ -63,7 +69,7 @@ impl MultisigUserApi for S { async fn as_multi( &self, - threshold: u16, + threshold: MultisigThreshold, other_signatories: Vec, timepoint: Option, max_weight: Weight, @@ -83,7 +89,7 @@ impl MultisigUserApi for S { async fn approve_as_multi( &self, - threshold: u16, + threshold: MultisigThreshold, other_signatories: Vec, timepoint: Option, max_weight: Weight, @@ -103,7 +109,7 @@ impl MultisigUserApi for S { async fn cancel_as_multi( &self, - threshold: u16, + threshold: MultisigThreshold, other_signatories: Vec, timepoint: Timepoint, call_hash: CallHash, @@ -119,3 +125,292 @@ impl MultisigUserApi for S { self.send_tx(tx, status).await } } + +#[derive(Clone, Eq, PartialEq, Debug)] +pub struct MultisigParty { + signatories: Vec, + threshold: MultisigThreshold, +} + +impl MultisigParty { + // no upperbound check + pub fn new(signatories: &[AccountId], threshold: MultisigThreshold) -> AnyResult { + let mut sorted_signatories = signatories.to_vec(); + sorted_signatories.sort(); + sorted_signatories.dedup(); + + ensure!( + sorted_signatories.len() > 1, + "There must be at least 2 different signatories" + ); + ensure!( + sorted_signatories.len() >= threshold as usize, + "Threshold must not be greater than the number of unique signatories" + ); + ensure!( + threshold >= 2, + "Threshold must be at least 2 - for threshold 1, use `as_multi_threshold_1`" + ); + + Ok(Self { + signatories: sorted_signatories, + threshold, + }) + } + + pub fn account(&self) -> AccountId { + let entropy = + (b"modlpy/utilisuba", &self.signatories, &self.threshold).using_encoded(blake2_256); + Decode::decode(&mut TrailingZeroInput::new(entropy.as_ref())) + .expect("infinite length input; no invalid inputs for type; qed") + } +} + +#[derive(Clone, Eq, PartialEq, Debug)] +pub struct Context { + party: MultisigParty, + author: AccountId, + timepoint: Timepoint, + max_weight: Weight, + call: Option, + call_hash: CallHash, +} + +impl Context { + pub fn change_max_weight(&mut self, max_weight: Weight) { + self.max_weight = max_weight; + } + + fn set_call(&mut self, call: &Call) -> anyhow::Result<()> { + ensure!( + self.call_hash == compute_call_hash(call), + "Call doesn't match to the registered hash" + ); + self.call = Some(call.clone()); + Ok(()) + } +} + +#[async_trait::async_trait] +pub trait MultisigApiExt { + async fn get_timepoint( + &self, + party_account: &AccountId, + call_hash: &CallHash, + block_hash: Option, + ) -> Timepoint; +} + +#[async_trait::async_trait] +impl MultisigApiExt for C { + async fn get_timepoint( + &self, + party_account: &AccountId, + call_hash: &CallHash, + block_hash: Option, + ) -> Timepoint { + let multisigs = api::storage() + .multisig() + .multisigs(party_account, call_hash); + let Multisig { when, .. } = self.get_storage_entry(&multisigs, block_hash).await; + when + } +} + +#[async_trait::async_trait] +pub trait MultisigContextualApi { + async fn initiate( + &self, + party: &MultisigParty, + max_weight: &Weight, + call_hash: CallHash, + status: TxStatus, + ) -> anyhow::Result<(BlockHash, Context)>; + async fn initiate_with_call( + &self, + party: &MultisigParty, + max_weight: &Weight, + call: Call, + status: TxStatus, + ) -> anyhow::Result<(BlockHash, Context)>; + async fn approve( + &self, + context: Context, + status: TxStatus, + ) -> anyhow::Result<(BlockHash, Context)>; + async fn approve_with_call( + &self, + context: Context, + call: Option, + status: TxStatus, + ) -> anyhow::Result<(BlockHash, Context)>; + async fn cancel(&self, context: Context, status: TxStatus) -> anyhow::Result; +} + +#[async_trait::async_trait] +impl MultisigContextualApi for S { + async fn initiate( + &self, + party: &MultisigParty, + max_weight: &Weight, + call_hash: CallHash, + status: TxStatus, + ) -> AnyResult<(BlockHash, Context)> { + let other_signatories = ensure_signer_in_party(self, party)?; + + let block_hash = self + .approve_as_multi( + party.threshold, + other_signatories, + None, + max_weight.clone(), + call_hash, + status, + ) + .await?; + + let timepoint = self + .get_timepoint(&party.account(), &call_hash, Some(block_hash)) + .await; + + Ok(( + block_hash, + Context { + party: party.clone(), + author: account_from_keypair(self.signer().signer()), + timepoint, + max_weight: max_weight.clone(), + call: None, + call_hash, + }, + )) + } + + async fn initiate_with_call( + &self, + party: &MultisigParty, + max_weight: &Weight, + call: Call, + status: TxStatus, + ) -> AnyResult<(BlockHash, Context)> { + let other_signatories = ensure_signer_in_party(self, party)?; + + let block_hash = self + .as_multi( + party.threshold, + other_signatories, + None, + max_weight.clone(), + call.clone(), + status, + ) + .await?; + + let call_hash = compute_call_hash(&call); + let timepoint = self + .get_timepoint(&party.account(), &call_hash, Some(block_hash)) + .await; + + Ok(( + block_hash, + Context { + party: party.clone(), + author: account_from_keypair(self.signer().signer()), + timepoint, + max_weight: max_weight.clone(), + call: Some(call.clone()), + call_hash, + }, + )) + } + + async fn approve(&self, context: Context, status: TxStatus) -> AnyResult<(BlockHash, Context)> { + let other_signatories = ensure_signer_in_party(self, &context.party)?; + + self.approve_as_multi( + context.party.threshold, + other_signatories, + Some(context.timepoint.clone()), + context.max_weight.clone(), + context.call_hash, + status, + ) + .await + .map(|block_hash| (block_hash, context)) + } + + async fn approve_with_call( + &self, + mut context: Context, + call: Option, + status: TxStatus, + ) -> AnyResult<(BlockHash, Context)> { + let other_signatories = ensure_signer_in_party(self, &context.party)?; + + let call = match (call.as_ref(), context.call.as_ref()) { + (None, None) => Err(anyhow!( + "Call wasn't provided earlier - you must pass it now" + )), + (None, Some(call)) => Ok(call), + (Some(call), None) => { + context.set_call(call)?; + Ok(call) + } + (Some(saved_call), Some(new_call)) => { + ensure!( + saved_call == new_call, + "The call is different that the one used previously" + ); + Ok(new_call) + } + }?; + + self.as_multi( + context.party.threshold, + other_signatories, + Some(context.timepoint.clone()), + context.max_weight.clone(), + call.clone(), + status, + ) + .await + .map(|block_hash| (block_hash, context)) + } + + async fn cancel(&self, context: Context, status: TxStatus) -> AnyResult { + let other_signatories = ensure_signer_in_party(self, &context.party)?; + + let signer = account_from_keypair(self.signer().signer()); + ensure!( + signer == context.author, + "Only the author can cancel multisig aggregation" + ); + + self.cancel_as_multi( + context.party.threshold, + other_signatories, + context.timepoint, + context.call_hash, + status, + ) + .await + } +} + +fn compute_call_hash(call: &Call) -> CallHash { + call.using_encoded(blake2_256) +} + +fn ensure_signer_in_party( + conn: &S, + party: &MultisigParty, +) -> anyhow::Result> { + let signer_account = account_from_keypair(conn.signer().signer()); + if let Ok(index) = party.signatories.binary_search(&signer_account) { + let mut other_signatories = party.signatories.clone(); + other_signatories.remove(index); + Ok(other_signatories) + } else { + Err(anyhow!("Connection should be signed by a party member")) + } +} From e57097056f3c0dc3632d69c80b62c090be417cb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Thu, 5 Jan 2023 10:23:49 +0100 Subject: [PATCH 03/11] Playing --- aleph-client/src/pallets/multisig.rs | 37 +++++++++++++--- aleph-client/src/runtime_types.rs | 2 +- bin/cliain/src/commands.rs | 2 + bin/cliain/src/main.rs | 64 +++++++++++++++++++++++++++- scripts/run_nodes.sh | 2 +- 5 files changed, 97 insertions(+), 10 deletions(-) diff --git a/aleph-client/src/pallets/multisig.rs b/aleph-client/src/pallets/multisig.rs index e60cf41401..5f3524b10f 100644 --- a/aleph-client/src/pallets/multisig.rs +++ b/aleph-client/src/pallets/multisig.rs @@ -16,6 +16,8 @@ pub type MultisigThreshold = u16; pub type Timepoint = runtime_types::pallet_multisig::Timepoint; pub type Multisig = runtime_types::pallet_multisig::Multisig; +pub const DEFAULT_MAX_WEIGHT: Weight = Weight::new(500_000_000, 0); + #[async_trait::async_trait] pub trait MultisigUserApi { async fn as_multi_threshold_1( @@ -170,10 +172,14 @@ impl MultisigParty { pub struct Context { party: MultisigParty, author: AccountId, + timepoint: Timepoint, max_weight: Weight, + call: Option, call_hash: CallHash, + + approvals: usize, } impl Context { @@ -189,6 +195,17 @@ impl Context { self.call = Some(call.clone()); Ok(()) } + + fn bump_approvals(self) -> Option { + if self.approvals + 1 >= (self.party.threshold as usize) { + None + } else { + Some(Context { + approvals: self.approvals + 1, + ..self + }) + } + } } #[async_trait::async_trait] @@ -237,13 +254,13 @@ pub trait MultisigContextualApi { &self, context: Context, status: TxStatus, - ) -> anyhow::Result<(BlockHash, Context)>; + ) -> anyhow::Result<(BlockHash, Option)>; async fn approve_with_call( &self, context: Context, call: Option, status: TxStatus, - ) -> anyhow::Result<(BlockHash, Context)>; + ) -> anyhow::Result<(BlockHash, Option)>; async fn cancel(&self, context: Context, status: TxStatus) -> anyhow::Result; } @@ -282,6 +299,7 @@ impl MultisigContextualApi for S { max_weight: max_weight.clone(), call: None, call_hash, + approvals: 1, }, )) } @@ -320,11 +338,16 @@ impl MultisigContextualApi for S { max_weight: max_weight.clone(), call: Some(call.clone()), call_hash, + approvals: 1, }, )) } - async fn approve(&self, context: Context, status: TxStatus) -> AnyResult<(BlockHash, Context)> { + async fn approve( + &self, + context: Context, + status: TxStatus, + ) -> AnyResult<(BlockHash, Option)> { let other_signatories = ensure_signer_in_party(self, &context.party)?; self.approve_as_multi( @@ -336,7 +359,7 @@ impl MultisigContextualApi for S { status, ) .await - .map(|block_hash| (block_hash, context)) + .map(|block_hash| (block_hash, context.bump_approvals())) } async fn approve_with_call( @@ -344,7 +367,7 @@ impl MultisigContextualApi for S { mut context: Context, call: Option, status: TxStatus, - ) -> AnyResult<(BlockHash, Context)> { + ) -> AnyResult<(BlockHash, Option)> { let other_signatories = ensure_signer_in_party(self, &context.party)?; let call = match (call.as_ref(), context.call.as_ref()) { @@ -374,7 +397,7 @@ impl MultisigContextualApi for S { status, ) .await - .map(|block_hash| (block_hash, context)) + .map(|block_hash| (block_hash, context.bump_approvals())) } async fn cancel(&self, context: Context, status: TxStatus) -> AnyResult { @@ -397,7 +420,7 @@ impl MultisigContextualApi for S { } } -fn compute_call_hash(call: &Call) -> CallHash { +pub fn compute_call_hash(call: &Call) -> CallHash { call.using_encoded(blake2_256) } diff --git a/aleph-client/src/runtime_types.rs b/aleph-client/src/runtime_types.rs index 2c1eceddbe..4b3be2a442 100644 --- a/aleph-client/src/runtime_types.rs +++ b/aleph-client/src/runtime_types.rs @@ -51,7 +51,7 @@ impl TryFrom for SessionKeys { } impl Weight { - pub fn new(ref_time: u64, proof_size: u64) -> Self { + pub const fn new(ref_time: u64, proof_size: u64) -> Self { Self { ref_time, proof_size, diff --git a/bin/cliain/src/commands.rs b/bin/cliain/src/commands.rs index 1847a6f849..e0004271e6 100644 --- a/bin/cliain/src/commands.rs +++ b/bin/cliain/src/commands.rs @@ -361,4 +361,6 @@ pub enum Command { #[clap(long, value_enum, default_value_t=ExtrinsicState::Finalized)] expected_state: ExtrinsicState, }, + + Multisig, } diff --git a/bin/cliain/src/main.rs b/bin/cliain/src/main.rs index ff06959770..123c8c96ce 100644 --- a/bin/cliain/src/main.rs +++ b/bin/cliain/src/main.rs @@ -1,6 +1,16 @@ use std::env; -use aleph_client::{account_from_keypair, aleph_keypair_from_string, keypair_from_string, Pair}; +use aleph_client::{ + account_from_keypair, aleph_keypair_from_string, + aleph_runtime::RuntimeCall, + keypair_from_string, + pallet_balances::pallet::Call, + pallets::multisig::{ + compute_call_hash, MultisigContextualApi, MultisigParty, DEFAULT_MAX_WEIGHT, + }, + Pair, SignedConnection, + TxStatus::Finalized, +}; use clap::Parser; use cliain::{ bond, call, change_validators, finalize, force_new_era, instantiate, instantiate_with_code, @@ -244,6 +254,58 @@ async fn main() -> anyhow::Result<()> { Ok(_) => {} Err(why) => error!("Unable to schedule an upgrade {:?}", why), }, + + Command::Multisig => { + let _0 = keypair_from_string("//0"); + let _1 = keypair_from_string("//1"); + let _2 = keypair_from_string("//2"); + + let keys = [ + keypair_from_string("//0"), + keypair_from_string("//1"), + keypair_from_string("//2"), + ]; + let accounts = keys + .iter() + .map(|k| account_from_keypair(k.signer())) + .collect::>(); + + let threshold = 3; + let party = + MultisigParty::new(&accounts, threshold).expect("Failed to create multisig party"); + + let call = RuntimeCall::Balances(Call::transfer { + dest: accounts[1].clone().into(), + value: 0, + }); + + let conn_0 = SignedConnection::from_connection(cfg.get_connection().await, _0); + let (_, context) = conn_0 + .initiate( + &party, + &DEFAULT_MAX_WEIGHT, + compute_call_hash(&call), + Finalized, + ) + .await + .expect("failed to initiate multisig aggregation"); + + let conn_1 = SignedConnection::from_connection(cfg.get_connection().await, _1); + let (_, context) = conn_1 + .approve(context, Finalized) + .await + .expect("failed to approve"); + + let context = context.expect("Failed to return context"); + + let conn_2 = SignedConnection::from_connection(cfg.get_connection().await, _2); + let (_, context) = conn_2 + .approve_with_call(context, Some(call), Finalized) + .await + .expect("failed to execute"); + + assert!(context.is_none(), "Failed to conclude aggregation"); + } } Ok(()) } diff --git a/scripts/run_nodes.sh b/scripts/run_nodes.sh index 3367deefb7..c868224fd9 100755 --- a/scripts/run_nodes.sh +++ b/scripts/run_nodes.sh @@ -38,7 +38,7 @@ clear if $BUILD_ALEPH_NODE ; then - cargo build --release -p aleph-node --features "short_session enable_treasury_proposals" + cargo build -j 6 --release -p aleph-node --features "short_session enable_treasury_proposals" fi declare -a account_ids From 8119b2548b749f6296d42a8060320510869cc050 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Thu, 5 Jan 2023 10:31:08 +0100 Subject: [PATCH 04/11] Keep approvers instead of counter --- aleph-client/src/pallets/multisig.rs | 38 ++++++++++++++++++---------- bin/cliain/src/main.rs | 1 + 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/aleph-client/src/pallets/multisig.rs b/aleph-client/src/pallets/multisig.rs index 5f3524b10f..b00fc05dba 100644 --- a/aleph-client/src/pallets/multisig.rs +++ b/aleph-client/src/pallets/multisig.rs @@ -1,3 +1,5 @@ +use std::collections::HashSet; + use anyhow::{anyhow, ensure, Result as AnyResult}; use codec::{Decode, Encode}; use primitives::{Balance, BlockNumber}; @@ -179,7 +181,7 @@ pub struct Context { call: Option, call_hash: CallHash, - approvals: usize, + approvers: HashSet, } impl Context { @@ -196,14 +198,12 @@ impl Context { Ok(()) } - fn bump_approvals(self) -> Option { - if self.approvals + 1 >= (self.party.threshold as usize) { + fn add_approval(mut self, approver: AccountId) -> Option { + self.approvers.insert(approver); + if self.approvers.len() >= (self.party.threshold as usize) { None } else { - Some(Context { - approvals: self.approvals + 1, - ..self - }) + Some(self) } } } @@ -289,17 +289,18 @@ impl MultisigContextualApi for S { let timepoint = self .get_timepoint(&party.account(), &call_hash, Some(block_hash)) .await; + let author = account_from_keypair(self.signer().signer()); Ok(( block_hash, Context { party: party.clone(), - author: account_from_keypair(self.signer().signer()), + author: author.clone(), timepoint, max_weight: max_weight.clone(), call: None, call_hash, - approvals: 1, + approvers: HashSet::from([author]), }, )) } @@ -328,17 +329,18 @@ impl MultisigContextualApi for S { let timepoint = self .get_timepoint(&party.account(), &call_hash, Some(block_hash)) .await; + let author = account_from_keypair(self.signer().signer()); Ok(( block_hash, Context { party: party.clone(), - author: account_from_keypair(self.signer().signer()), + author: author.clone(), timepoint, max_weight: max_weight.clone(), call: Some(call.clone()), call_hash, - approvals: 1, + approvers: HashSet::from([author]), }, )) } @@ -359,7 +361,12 @@ impl MultisigContextualApi for S { status, ) .await - .map(|block_hash| (block_hash, context.bump_approvals())) + .map(|block_hash| { + ( + block_hash, + context.add_approval(account_from_keypair(self.signer().signer())), + ) + }) } async fn approve_with_call( @@ -397,7 +404,12 @@ impl MultisigContextualApi for S { status, ) .await - .map(|block_hash| (block_hash, context.bump_approvals())) + .map(|block_hash| { + ( + block_hash, + context.add_approval(account_from_keypair(self.signer().signer())), + ) + }) } async fn cancel(&self, context: Context, status: TxStatus) -> AnyResult { diff --git a/bin/cliain/src/main.rs b/bin/cliain/src/main.rs index 123c8c96ce..cbf16b376b 100644 --- a/bin/cliain/src/main.rs +++ b/bin/cliain/src/main.rs @@ -46,6 +46,7 @@ fn read_seed(command: &Command, seed: Option) -> String { hash: _, finalizer_seed: _, } + | Command::Multisig | Command::NextSessionKeys { account_id: _ } | Command::RotateKeys | Command::SeedToSS58 { input: _ } From d7f377ba8237c7da314723fc7295837d8c60ba17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Thu, 5 Jan 2023 10:31:51 +0100 Subject: [PATCH 05/11] revert local playing --- bin/cliain/src/commands.rs | 2 -- bin/cliain/src/main.rs | 65 +------------------------------------- scripts/run_nodes.sh | 2 +- 3 files changed, 2 insertions(+), 67 deletions(-) diff --git a/bin/cliain/src/commands.rs b/bin/cliain/src/commands.rs index e0004271e6..1847a6f849 100644 --- a/bin/cliain/src/commands.rs +++ b/bin/cliain/src/commands.rs @@ -361,6 +361,4 @@ pub enum Command { #[clap(long, value_enum, default_value_t=ExtrinsicState::Finalized)] expected_state: ExtrinsicState, }, - - Multisig, } diff --git a/bin/cliain/src/main.rs b/bin/cliain/src/main.rs index cbf16b376b..ff06959770 100644 --- a/bin/cliain/src/main.rs +++ b/bin/cliain/src/main.rs @@ -1,16 +1,6 @@ use std::env; -use aleph_client::{ - account_from_keypair, aleph_keypair_from_string, - aleph_runtime::RuntimeCall, - keypair_from_string, - pallet_balances::pallet::Call, - pallets::multisig::{ - compute_call_hash, MultisigContextualApi, MultisigParty, DEFAULT_MAX_WEIGHT, - }, - Pair, SignedConnection, - TxStatus::Finalized, -}; +use aleph_client::{account_from_keypair, aleph_keypair_from_string, keypair_from_string, Pair}; use clap::Parser; use cliain::{ bond, call, change_validators, finalize, force_new_era, instantiate, instantiate_with_code, @@ -46,7 +36,6 @@ fn read_seed(command: &Command, seed: Option) -> String { hash: _, finalizer_seed: _, } - | Command::Multisig | Command::NextSessionKeys { account_id: _ } | Command::RotateKeys | Command::SeedToSS58 { input: _ } @@ -255,58 +244,6 @@ async fn main() -> anyhow::Result<()> { Ok(_) => {} Err(why) => error!("Unable to schedule an upgrade {:?}", why), }, - - Command::Multisig => { - let _0 = keypair_from_string("//0"); - let _1 = keypair_from_string("//1"); - let _2 = keypair_from_string("//2"); - - let keys = [ - keypair_from_string("//0"), - keypair_from_string("//1"), - keypair_from_string("//2"), - ]; - let accounts = keys - .iter() - .map(|k| account_from_keypair(k.signer())) - .collect::>(); - - let threshold = 3; - let party = - MultisigParty::new(&accounts, threshold).expect("Failed to create multisig party"); - - let call = RuntimeCall::Balances(Call::transfer { - dest: accounts[1].clone().into(), - value: 0, - }); - - let conn_0 = SignedConnection::from_connection(cfg.get_connection().await, _0); - let (_, context) = conn_0 - .initiate( - &party, - &DEFAULT_MAX_WEIGHT, - compute_call_hash(&call), - Finalized, - ) - .await - .expect("failed to initiate multisig aggregation"); - - let conn_1 = SignedConnection::from_connection(cfg.get_connection().await, _1); - let (_, context) = conn_1 - .approve(context, Finalized) - .await - .expect("failed to approve"); - - let context = context.expect("Failed to return context"); - - let conn_2 = SignedConnection::from_connection(cfg.get_connection().await, _2); - let (_, context) = conn_2 - .approve_with_call(context, Some(call), Finalized) - .await - .expect("failed to execute"); - - assert!(context.is_none(), "Failed to conclude aggregation"); - } } Ok(()) } diff --git a/scripts/run_nodes.sh b/scripts/run_nodes.sh index c868224fd9..3367deefb7 100755 --- a/scripts/run_nodes.sh +++ b/scripts/run_nodes.sh @@ -38,7 +38,7 @@ clear if $BUILD_ALEPH_NODE ; then - cargo build -j 6 --release -p aleph-node --features "short_session enable_treasury_proposals" + cargo build --release -p aleph-node --features "short_session enable_treasury_proposals" fi declare -a account_ids From caa8cdc17c87739be58edb3f0804897ef3ff3a14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Thu, 5 Jan 2023 11:05:30 +0100 Subject: [PATCH 06/11] Docs --- aleph-client/src/pallets/multisig.rs | 69 +++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/aleph-client/src/pallets/multisig.rs b/aleph-client/src/pallets/multisig.rs index b00fc05dba..e94411d328 100644 --- a/aleph-client/src/pallets/multisig.rs +++ b/aleph-client/src/pallets/multisig.rs @@ -130,6 +130,7 @@ impl MultisigUserApi for S { } } +/// A group of accounts together with a threshold. #[derive(Clone, Eq, PartialEq, Debug)] pub struct MultisigParty { signatories: Vec, @@ -137,7 +138,15 @@ pub struct MultisigParty { } impl MultisigParty { - // no upperbound check + /// Create new party from `signatories` and `threshold`. + /// + /// `signatories` can contain duplicates and doesn't have to be sorted. However, there must be + /// at least 2 unique accounts. There is also a virtual upper bound - `MaxSignatories` constant. + /// It isn't checked here (since it requires client), however, using too big party will fail + /// when performing any chain interaction. + /// + /// `threshold` must be between 2 and number of unique accounts in `signatories`. For threshold + /// 1, use special method `MultisigUserApi::as_multi_threshold_1`. pub fn new(signatories: &[AccountId], threshold: MultisigThreshold) -> AnyResult { let mut sorted_signatories = signatories.to_vec(); sorted_signatories.sort(); @@ -162,6 +171,12 @@ impl MultisigParty { }) } + /// The multisig account derived from signatories and threshold. + /// + /// This method is copied from the pallet, because: + /// - we don't want to add a new dependency + /// - we cannot instantiate pallet object here anyway (the corresponding functionality exists + /// as pallet's method rather than standalone function) pub fn account(&self) -> AccountId { let entropy = (b"modlpy/utilisuba", &self.signatories, &self.threshold).using_encoded(blake2_256); @@ -170,25 +185,41 @@ impl MultisigParty { } } +/// A context in which ongoing signature aggregation is performed. #[derive(Clone, Eq, PartialEq, Debug)] pub struct Context { + /// The entity for which aggregation is being made. party: MultisigParty, + /// Derived multisig account (the source of the target call). author: AccountId, + /// Pallet's coordinate for this aggregation. timepoint: Timepoint, + /// Weight limit when dispatching the call. max_weight: Weight, + /// The target dispatchable, if already provided. call: Option, + /// The hash of the target dispatchable. call_hash: CallHash, + /// The set of accounts, that already approved the call (via this context object), including the + /// author. + /// + /// `approvers.len() < party.threshold` always holds. approvers: HashSet, } impl Context { + /// In case `Context` object has been passed somewhere, where this limit should be adjusted, we + /// allow for that. + /// + /// Actually, this isn't used until threshold is met, so such changing is perfectly safe. pub fn change_max_weight(&mut self, max_weight: Weight) { self.max_weight = max_weight; } + /// Set `call` only if `self.call_hash` is matching. fn set_call(&mut self, call: &Call) -> anyhow::Result<()> { ensure!( self.call_hash == compute_call_hash(call), @@ -198,9 +229,11 @@ impl Context { Ok(()) } + /// Register another approval. Consume `self` if the threshold has been met and `call` is + /// already known. fn add_approval(mut self, approver: AccountId) -> Option { self.approvers.insert(approver); - if self.approvers.len() >= (self.party.threshold as usize) { + if self.call.is_some() && self.approvers.len() >= (self.party.threshold as usize) { None } else { Some(self) @@ -208,8 +241,11 @@ impl Context { } } +/// Pallet multisig functionality that is not directly related to any pallet call. #[async_trait::async_trait] pub trait MultisigApiExt { + /// Get the coordinate that corresponds to the ongoing signature aggregation for `party_account` + /// and `call_hash`. async fn get_timepoint( &self, party_account: &AccountId, @@ -234,8 +270,14 @@ impl MultisigApiExt for C { } } +/// Pallet multisig API, but suited for cases when the whole scenario is performed in a single place +/// - we keep data in a context object which helps in concise programming. #[async_trait::async_trait] pub trait MultisigContextualApi { + /// Start signature aggregation for `party` and `call_hash`. Get `Context` object as a result + /// (together with standard block hash). + /// + /// This is the recommended way of initialization. async fn initiate( &self, party: &MultisigParty, @@ -243,6 +285,11 @@ pub trait MultisigContextualApi { call_hash: CallHash, status: TxStatus, ) -> anyhow::Result<(BlockHash, Context)>; + /// Start signature aggregation for `party` and `call`. Get `Context` object as a result + /// (together with standard block hash). + /// + /// Note: it is usually a better idea to pass `call` only with the final approval (so that it + /// isn't stored on-chain). async fn initiate_with_call( &self, party: &MultisigParty, @@ -250,11 +297,20 @@ pub trait MultisigContextualApi { call: Call, status: TxStatus, ) -> anyhow::Result<(BlockHash, Context)>; + /// Express contextual approval for the call hash. Get `Context` object back if the target + /// dispatchable couldn't have been executed yet (either too less approvals or only hash is + /// known). + /// + /// This is the recommended way for every intermediate approval. async fn approve( &self, context: Context, status: TxStatus, ) -> anyhow::Result<(BlockHash, Option)>; + /// Express contextual approval for the `call`. Get `Context` object back if the threshold is + /// still not met. + /// + /// This is the recommended way only for the final approval. async fn approve_with_call( &self, context: Context, @@ -286,6 +342,12 @@ impl MultisigContextualApi for S { ) .await?; + // Even though `subxt` allows us to get timepoint when waiting for the submission + // confirmation (see e.g. `ExtrinsicEvents` object that is returned from + // `wait_for_finalized_success`), we chose to perform one additional storage read. + // Firstly, because of brevity here (we would have to duplicate some lines from + // `connections` module. Secondly, if `Timepoint` struct change, this method (reading raw + // extrinsic position) might become incorrect. let timepoint = self .get_timepoint(&party.account(), &call_hash, Some(block_hash)) .await; @@ -432,10 +494,13 @@ impl MultisigContextualApi for S { } } +/// Compute hash of `call`. pub fn compute_call_hash(call: &Call) -> CallHash { call.using_encoded(blake2_256) } +/// Ensure that the signer of `conn` is present in `party.signatories`. If so, return all other +/// signatories. fn ensure_signer_in_party( conn: &S, party: &MultisigParty, From 57367a0595eeb9ae353e0524f80764a404d4f85a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Thu, 5 Jan 2023 15:30:41 +0100 Subject: [PATCH 07/11] Easier review comments --- aleph-client/src/pallets/multisig.rs | 86 +++++++++++++++------------- 1 file changed, 45 insertions(+), 41 deletions(-) diff --git a/aleph-client/src/pallets/multisig.rs b/aleph-client/src/pallets/multisig.rs index e94411d328..549ff3063a 100644 --- a/aleph-client/src/pallets/multisig.rs +++ b/aleph-client/src/pallets/multisig.rs @@ -1,6 +1,6 @@ use std::collections::HashSet; -use anyhow::{anyhow, ensure, Result as AnyResult}; +use anyhow::{anyhow, ensure}; use codec::{Decode, Encode}; use primitives::{Balance, BlockNumber}; use sp_core::blake2_256; @@ -147,7 +147,7 @@ impl MultisigParty { /// /// `threshold` must be between 2 and number of unique accounts in `signatories`. For threshold /// 1, use special method `MultisigUserApi::as_multi_threshold_1`. - pub fn new(signatories: &[AccountId], threshold: MultisigThreshold) -> AnyResult { + pub fn new(signatories: &[AccountId], threshold: MultisigThreshold) -> anyhow::Result { let mut sorted_signatories = signatories.to_vec(); sorted_signatories.sort(); sorted_signatories.dedup(); @@ -211,6 +211,25 @@ pub struct Context { } impl Context { + fn new( + party: MultisigParty, + author: AccountId, + timepoint: Timepoint, + max_weight: Weight, + call: Option, + call_hash: CallHash, + ) -> Self { + Self { + party, + author: author.clone(), + timepoint, + max_weight, + call, + call_hash, + approvers: HashSet::from([author]), + } + } + /// In case `Context` object has been passed somewhere, where this limit should be adjusted, we /// allow for that. /// @@ -220,12 +239,12 @@ impl Context { } /// Set `call` only if `self.call_hash` is matching. - fn set_call(&mut self, call: &Call) -> anyhow::Result<()> { + fn set_call(&mut self, call: Call) -> anyhow::Result<()> { ensure!( - self.call_hash == compute_call_hash(call), + self.call_hash == compute_call_hash(&call), "Call doesn't match to the registered hash" ); - self.call = Some(call.clone()); + self.call = Some(call); Ok(()) } @@ -328,7 +347,7 @@ impl MultisigContextualApi for S { max_weight: &Weight, call_hash: CallHash, status: TxStatus, - ) -> AnyResult<(BlockHash, Context)> { + ) -> anyhow::Result<(BlockHash, Context)> { let other_signatories = ensure_signer_in_party(self, party)?; let block_hash = self @@ -351,19 +370,17 @@ impl MultisigContextualApi for S { let timepoint = self .get_timepoint(&party.account(), &call_hash, Some(block_hash)) .await; - let author = account_from_keypair(self.signer().signer()); Ok(( block_hash, - Context { - party: party.clone(), - author: author.clone(), + Context::new( + party.clone(), + self.account_id().clone(), timepoint, - max_weight: max_weight.clone(), - call: None, + max_weight.clone(), + None, call_hash, - approvers: HashSet::from([author]), - }, + ), )) } @@ -373,7 +390,7 @@ impl MultisigContextualApi for S { max_weight: &Weight, call: Call, status: TxStatus, - ) -> AnyResult<(BlockHash, Context)> { + ) -> anyhow::Result<(BlockHash, Context)> { let other_signatories = ensure_signer_in_party(self, party)?; let block_hash = self @@ -391,19 +408,17 @@ impl MultisigContextualApi for S { let timepoint = self .get_timepoint(&party.account(), &call_hash, Some(block_hash)) .await; - let author = account_from_keypair(self.signer().signer()); Ok(( block_hash, - Context { - party: party.clone(), - author: author.clone(), + Context::new( + party.clone(), + self.account_id().clone(), timepoint, - max_weight: max_weight.clone(), - call: Some(call.clone()), + max_weight.clone(), + Some(call.clone()), call_hash, - approvers: HashSet::from([author]), - }, + ), )) } @@ -411,7 +426,7 @@ impl MultisigContextualApi for S { &self, context: Context, status: TxStatus, - ) -> AnyResult<(BlockHash, Option)> { + ) -> anyhow::Result<(BlockHash, Option)> { let other_signatories = ensure_signer_in_party(self, &context.party)?; self.approve_as_multi( @@ -423,12 +438,7 @@ impl MultisigContextualApi for S { status, ) .await - .map(|block_hash| { - ( - block_hash, - context.add_approval(account_from_keypair(self.signer().signer())), - ) - }) + .map(|block_hash| (block_hash, context.add_approval(self.account_id().clone()))) } async fn approve_with_call( @@ -436,7 +446,7 @@ impl MultisigContextualApi for S { mut context: Context, call: Option, status: TxStatus, - ) -> AnyResult<(BlockHash, Option)> { + ) -> anyhow::Result<(BlockHash, Option)> { let other_signatories = ensure_signer_in_party(self, &context.party)?; let call = match (call.as_ref(), context.call.as_ref()) { @@ -445,7 +455,7 @@ impl MultisigContextualApi for S { )), (None, Some(call)) => Ok(call), (Some(call), None) => { - context.set_call(call)?; + context.set_call(call.clone())?; Ok(call) } (Some(saved_call), Some(new_call)) => { @@ -466,20 +476,14 @@ impl MultisigContextualApi for S { status, ) .await - .map(|block_hash| { - ( - block_hash, - context.add_approval(account_from_keypair(self.signer().signer())), - ) - }) + .map(|block_hash| (block_hash, context.add_approval(self.account_id().clone()))) } - async fn cancel(&self, context: Context, status: TxStatus) -> AnyResult { + async fn cancel(&self, context: Context, status: TxStatus) -> anyhow::Result { let other_signatories = ensure_signer_in_party(self, &context.party)?; - let signer = account_from_keypair(self.signer().signer()); ensure!( - signer == context.author, + *self.account_id() == context.author, "Only the author can cancel multisig aggregation" ); From a0232115fee405360dc60d95d0bf01b2d53a6001 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Thu, 5 Jan 2023 15:34:17 +0100 Subject: [PATCH 08/11] Move context code closer to ContextualApi --- aleph-client/src/pallets/multisig.rs | 58 ++++++++++++++-------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/aleph-client/src/pallets/multisig.rs b/aleph-client/src/pallets/multisig.rs index 549ff3063a..1ed62a3bf3 100644 --- a/aleph-client/src/pallets/multisig.rs +++ b/aleph-client/src/pallets/multisig.rs @@ -185,6 +185,35 @@ impl MultisigParty { } } +/// Pallet multisig functionality that is not directly related to any pallet call. +#[async_trait::async_trait] +pub trait MultisigApiExt { + /// Get the coordinate that corresponds to the ongoing signature aggregation for `party_account` + /// and `call_hash`. + async fn get_timepoint( + &self, + party_account: &AccountId, + call_hash: &CallHash, + block_hash: Option, + ) -> Timepoint; +} + +#[async_trait::async_trait] +impl MultisigApiExt for C { + async fn get_timepoint( + &self, + party_account: &AccountId, + call_hash: &CallHash, + block_hash: Option, + ) -> Timepoint { + let multisigs = api::storage() + .multisig() + .multisigs(party_account, call_hash); + let Multisig { when, .. } = self.get_storage_entry(&multisigs, block_hash).await; + when + } +} + /// A context in which ongoing signature aggregation is performed. #[derive(Clone, Eq, PartialEq, Debug)] pub struct Context { @@ -260,35 +289,6 @@ impl Context { } } -/// Pallet multisig functionality that is not directly related to any pallet call. -#[async_trait::async_trait] -pub trait MultisigApiExt { - /// Get the coordinate that corresponds to the ongoing signature aggregation for `party_account` - /// and `call_hash`. - async fn get_timepoint( - &self, - party_account: &AccountId, - call_hash: &CallHash, - block_hash: Option, - ) -> Timepoint; -} - -#[async_trait::async_trait] -impl MultisigApiExt for C { - async fn get_timepoint( - &self, - party_account: &AccountId, - call_hash: &CallHash, - block_hash: Option, - ) -> Timepoint { - let multisigs = api::storage() - .multisig() - .multisigs(party_account, call_hash); - let Multisig { when, .. } = self.get_storage_entry(&multisigs, block_hash).await; - when - } -} - /// Pallet multisig API, but suited for cases when the whole scenario is performed in a single place /// - we keep data in a context object which helps in concise programming. #[async_trait::async_trait] From 556fba5ac120d97ef400752231cd53375b04292c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Thu, 5 Jan 2023 15:56:44 +0100 Subject: [PATCH 09/11] Docs --- aleph-client/src/pallets/multisig.rs | 143 ++++++++++++++++++++------- 1 file changed, 108 insertions(+), 35 deletions(-) diff --git a/aleph-client/src/pallets/multisig.rs b/aleph-client/src/pallets/multisig.rs index 1ed62a3bf3..5d4d7f218e 100644 --- a/aleph-client/src/pallets/multisig.rs +++ b/aleph-client/src/pallets/multisig.rs @@ -1,4 +1,4 @@ -use std::collections::HashSet; +use std::{collections::HashSet, marker::PhantomData}; use anyhow::{anyhow, ensure}; use codec::{Decode, Encode}; @@ -214,9 +214,23 @@ impl MultisigApiExt for C { } } +/// We will mark context object as either ongoing procedure or a closed one. However, we put this +/// distinction to the type level, so instead of enum, we use a trait. +pub trait ContextState {} + +/// Context of the signature aggregation that is still in progress. +#[derive(Clone, Eq, PartialEq, Debug)] +pub enum Ongoing {} +impl ContextState for Ongoing {} + +/// Context of the signature aggregation that was either successfully performed or canceled. +#[derive(Clone, Eq, PartialEq, Debug)] +pub enum Closed {} +impl ContextState for Closed {} + /// A context in which ongoing signature aggregation is performed. #[derive(Clone, Eq, PartialEq, Debug)] -pub struct Context { +pub struct Context { /// The entity for which aggregation is being made. party: MultisigParty, /// Derived multisig account (the source of the target call). @@ -237,9 +251,19 @@ pub struct Context { /// /// `approvers.len() < party.threshold` always holds. approvers: HashSet, + + _phantom: PhantomData, } -impl Context { +/// After approval action, our context can be in two modes - either for further use (`Ongoing`), or +/// read only (`Closed`). +#[derive(Clone, Eq, PartialEq, Debug)] +pub enum ContextAfterUse { + Ongoing(Context), + Closed(Context), +} + +impl Context { fn new( party: MultisigParty, author: AccountId, @@ -256,6 +280,7 @@ impl Context { call, call_hash, approvers: HashSet::from([author]), + _phantom: PhantomData, } } @@ -277,18 +302,57 @@ impl Context { Ok(()) } - /// Register another approval. Consume `self` if the threshold has been met and `call` is - /// already known. - fn add_approval(mut self, approver: AccountId) -> Option { + /// Register another approval. Depending on the threshold meeting and `call` content, we treat + /// signature aggregation process as either still ongoing or closed. + fn add_approval(mut self, approver: AccountId) -> ContextAfterUse { self.approvers.insert(approver); if self.call.is_some() && self.approvers.len() >= (self.party.threshold as usize) { - None + ContextAfterUse::Closed(self.close()) } else { - Some(self) + ContextAfterUse::Ongoing(self) + } + } + + /// Casting to the closed variant. Private, so that the user don't accidentally call `into()` + /// and close ongoing context. + fn close(self) -> Context { + Context:: { + party: self.party, + author: self.author, + timepoint: self.timepoint, + max_weight: self.max_weight, + call: self.call, + call_hash: self.call_hash, + approvers: self.approvers, + _phantom: Default::default(), } } } +impl Context { + pub fn party(&self) -> &MultisigParty { + &self.party + } + pub fn author(&self) -> &AccountId { + &self.author + } + pub fn timepoint(&self) -> &Timepoint { + &self.timepoint + } + pub fn max_weight(&self) -> &Weight { + &self.max_weight + } + pub fn call(&self) -> &Option { + &self.call + } + pub fn call_hash(&self) -> CallHash { + self.call_hash + } + pub fn approvers(&self) -> &HashSet { + &self.approvers + } +} + /// Pallet multisig API, but suited for cases when the whole scenario is performed in a single place /// - we keep data in a context object which helps in concise programming. #[async_trait::async_trait] @@ -303,7 +367,7 @@ pub trait MultisigContextualApi { max_weight: &Weight, call_hash: CallHash, status: TxStatus, - ) -> anyhow::Result<(BlockHash, Context)>; + ) -> anyhow::Result<(BlockHash, Context)>; /// Start signature aggregation for `party` and `call`. Get `Context` object as a result /// (together with standard block hash). /// @@ -315,28 +379,30 @@ pub trait MultisigContextualApi { max_weight: &Weight, call: Call, status: TxStatus, - ) -> anyhow::Result<(BlockHash, Context)>; - /// Express contextual approval for the call hash. Get `Context` object back if the target - /// dispatchable couldn't have been executed yet (either too less approvals or only hash is - /// known). + ) -> anyhow::Result<(BlockHash, Context)>; + /// Express contextual approval for the call hash. /// /// This is the recommended way for every intermediate approval. async fn approve( &self, - context: Context, + context: Context, status: TxStatus, - ) -> anyhow::Result<(BlockHash, Option)>; - /// Express contextual approval for the `call`. Get `Context` object back if the threshold is - /// still not met. + ) -> anyhow::Result<(BlockHash, ContextAfterUse)>; + /// Express contextual approval for the `call`. /// /// This is the recommended way only for the final approval. async fn approve_with_call( &self, - context: Context, + context: Context, call: Option, status: TxStatus, - ) -> anyhow::Result<(BlockHash, Option)>; - async fn cancel(&self, context: Context, status: TxStatus) -> anyhow::Result; + ) -> anyhow::Result<(BlockHash, ContextAfterUse)>; + /// Cancel signature aggregation. + async fn cancel( + &self, + context: Context, + status: TxStatus, + ) -> anyhow::Result<(BlockHash, Context)>; } #[async_trait::async_trait] @@ -347,7 +413,7 @@ impl MultisigContextualApi for S { max_weight: &Weight, call_hash: CallHash, status: TxStatus, - ) -> anyhow::Result<(BlockHash, Context)> { + ) -> anyhow::Result<(BlockHash, Context)> { let other_signatories = ensure_signer_in_party(self, party)?; let block_hash = self @@ -390,7 +456,7 @@ impl MultisigContextualApi for S { max_weight: &Weight, call: Call, status: TxStatus, - ) -> anyhow::Result<(BlockHash, Context)> { + ) -> anyhow::Result<(BlockHash, Context)> { let other_signatories = ensure_signer_in_party(self, party)?; let block_hash = self @@ -424,9 +490,9 @@ impl MultisigContextualApi for S { async fn approve( &self, - context: Context, + context: Context, status: TxStatus, - ) -> anyhow::Result<(BlockHash, Option)> { + ) -> anyhow::Result<(BlockHash, ContextAfterUse)> { let other_signatories = ensure_signer_in_party(self, &context.party)?; self.approve_as_multi( @@ -443,10 +509,10 @@ impl MultisigContextualApi for S { async fn approve_with_call( &self, - mut context: Context, + mut context: Context, call: Option, status: TxStatus, - ) -> anyhow::Result<(BlockHash, Option)> { + ) -> anyhow::Result<(BlockHash, ContextAfterUse)> { let other_signatories = ensure_signer_in_party(self, &context.party)?; let call = match (call.as_ref(), context.call.as_ref()) { @@ -479,7 +545,11 @@ impl MultisigContextualApi for S { .map(|block_hash| (block_hash, context.add_approval(self.account_id().clone()))) } - async fn cancel(&self, context: Context, status: TxStatus) -> anyhow::Result { + async fn cancel( + &self, + context: Context, + status: TxStatus, + ) -> anyhow::Result<(BlockHash, Context)> { let other_signatories = ensure_signer_in_party(self, &context.party)?; ensure!( @@ -487,14 +557,17 @@ impl MultisigContextualApi for S { "Only the author can cancel multisig aggregation" ); - self.cancel_as_multi( - context.party.threshold, - other_signatories, - context.timepoint, - context.call_hash, - status, - ) - .await + let block_hash = self + .cancel_as_multi( + context.party.threshold, + other_signatories, + context.timepoint.clone(), + context.call_hash, + status, + ) + .await?; + + Ok((block_hash, context.close())) } } From bf1dbdc95921462b0678e9a12ec3e9fefd1b9cd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Thu, 5 Jan 2023 16:00:40 +0100 Subject: [PATCH 10/11] playground updated --- bin/cliain/src/commands.rs | 2 ++ bin/cliain/src/main.rs | 72 +++++++++++++++++++++++++++++++++++++- 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/bin/cliain/src/commands.rs b/bin/cliain/src/commands.rs index 1847a6f849..e0004271e6 100644 --- a/bin/cliain/src/commands.rs +++ b/bin/cliain/src/commands.rs @@ -361,4 +361,6 @@ pub enum Command { #[clap(long, value_enum, default_value_t=ExtrinsicState::Finalized)] expected_state: ExtrinsicState, }, + + Multisig, } diff --git a/bin/cliain/src/main.rs b/bin/cliain/src/main.rs index ff06959770..0fac39cb19 100644 --- a/bin/cliain/src/main.rs +++ b/bin/cliain/src/main.rs @@ -1,6 +1,17 @@ use std::env; -use aleph_client::{account_from_keypair, aleph_keypair_from_string, keypair_from_string, Pair}; +use aleph_client::{ + account_from_keypair, aleph_keypair_from_string, + aleph_runtime::RuntimeCall, + keypair_from_string, + pallet_balances::pallet::Call, + pallets::multisig::{ + compute_call_hash, ContextAfterUse, MultisigContextualApi, MultisigParty, + DEFAULT_MAX_WEIGHT, + }, + Pair, SignedConnection, + TxStatus::Finalized, +}; use clap::Parser; use cliain::{ bond, call, change_validators, finalize, force_new_era, instantiate, instantiate_with_code, @@ -36,6 +47,7 @@ fn read_seed(command: &Command, seed: Option) -> String { hash: _, finalizer_seed: _, } + | Command::Multisig | Command::NextSessionKeys { account_id: _ } | Command::RotateKeys | Command::SeedToSS58 { input: _ } @@ -244,6 +256,64 @@ async fn main() -> anyhow::Result<()> { Ok(_) => {} Err(why) => error!("Unable to schedule an upgrade {:?}", why), }, + + Command::Multisig => { + let _0 = keypair_from_string("//0"); + let _1 = keypair_from_string("//1"); + let _2 = keypair_from_string("//2"); + + let keys = [ + keypair_from_string("//0"), + keypair_from_string("//1"), + keypair_from_string("//2"), + ]; + let accounts = keys + .iter() + .map(|k| account_from_keypair(k.signer())) + .collect::>(); + + let threshold = 3; + let party = + MultisigParty::new(&accounts, threshold).expect("Failed to create multisig party"); + + let call = RuntimeCall::Balances(Call::transfer { + dest: accounts[1].clone().into(), + value: 0, + }); + + let conn_0 = SignedConnection::from_connection(cfg.get_connection().await, _0); + let (_, context) = conn_0 + .initiate( + &party, + &DEFAULT_MAX_WEIGHT, + compute_call_hash(&call), + Finalized, + ) + .await + .expect("failed to initiate multisig aggregation"); + + let conn_1 = SignedConnection::from_connection(cfg.get_connection().await, _1); + let (_, context) = conn_1 + .approve(context, Finalized) + .await + .expect("failed to approve"); + + let context = match context { + ContextAfterUse::Ongoing(context) => context, + ContextAfterUse::Closed(_) => panic!("Process should continue"), + }; + + let conn_2 = SignedConnection::from_connection(cfg.get_connection().await, _2); + let (_, context) = conn_2 + .approve_with_call(context, Some(call), Finalized) + .await + .expect("failed to execute"); + + match context { + ContextAfterUse::Ongoing(_) => panic!("Failed to conclude aggregation"), + ContextAfterUse::Closed(_) => {} + }; + } } Ok(()) } From 7449ecc812d117d18e828fc3ba5c7dba6f17c694 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Thu, 5 Jan 2023 16:00:49 +0100 Subject: [PATCH 11/11] Revert "playground updated" This reverts commit bf1dbdc95921462b0678e9a12ec3e9fefd1b9cd6. --- bin/cliain/src/commands.rs | 2 -- bin/cliain/src/main.rs | 72 +------------------------------------- 2 files changed, 1 insertion(+), 73 deletions(-) diff --git a/bin/cliain/src/commands.rs b/bin/cliain/src/commands.rs index e0004271e6..1847a6f849 100644 --- a/bin/cliain/src/commands.rs +++ b/bin/cliain/src/commands.rs @@ -361,6 +361,4 @@ pub enum Command { #[clap(long, value_enum, default_value_t=ExtrinsicState::Finalized)] expected_state: ExtrinsicState, }, - - Multisig, } diff --git a/bin/cliain/src/main.rs b/bin/cliain/src/main.rs index 0fac39cb19..ff06959770 100644 --- a/bin/cliain/src/main.rs +++ b/bin/cliain/src/main.rs @@ -1,17 +1,6 @@ use std::env; -use aleph_client::{ - account_from_keypair, aleph_keypair_from_string, - aleph_runtime::RuntimeCall, - keypair_from_string, - pallet_balances::pallet::Call, - pallets::multisig::{ - compute_call_hash, ContextAfterUse, MultisigContextualApi, MultisigParty, - DEFAULT_MAX_WEIGHT, - }, - Pair, SignedConnection, - TxStatus::Finalized, -}; +use aleph_client::{account_from_keypair, aleph_keypair_from_string, keypair_from_string, Pair}; use clap::Parser; use cliain::{ bond, call, change_validators, finalize, force_new_era, instantiate, instantiate_with_code, @@ -47,7 +36,6 @@ fn read_seed(command: &Command, seed: Option) -> String { hash: _, finalizer_seed: _, } - | Command::Multisig | Command::NextSessionKeys { account_id: _ } | Command::RotateKeys | Command::SeedToSS58 { input: _ } @@ -256,64 +244,6 @@ async fn main() -> anyhow::Result<()> { Ok(_) => {} Err(why) => error!("Unable to schedule an upgrade {:?}", why), }, - - Command::Multisig => { - let _0 = keypair_from_string("//0"); - let _1 = keypair_from_string("//1"); - let _2 = keypair_from_string("//2"); - - let keys = [ - keypair_from_string("//0"), - keypair_from_string("//1"), - keypair_from_string("//2"), - ]; - let accounts = keys - .iter() - .map(|k| account_from_keypair(k.signer())) - .collect::>(); - - let threshold = 3; - let party = - MultisigParty::new(&accounts, threshold).expect("Failed to create multisig party"); - - let call = RuntimeCall::Balances(Call::transfer { - dest: accounts[1].clone().into(), - value: 0, - }); - - let conn_0 = SignedConnection::from_connection(cfg.get_connection().await, _0); - let (_, context) = conn_0 - .initiate( - &party, - &DEFAULT_MAX_WEIGHT, - compute_call_hash(&call), - Finalized, - ) - .await - .expect("failed to initiate multisig aggregation"); - - let conn_1 = SignedConnection::from_connection(cfg.get_connection().await, _1); - let (_, context) = conn_1 - .approve(context, Finalized) - .await - .expect("failed to approve"); - - let context = match context { - ContextAfterUse::Ongoing(context) => context, - ContextAfterUse::Closed(_) => panic!("Process should continue"), - }; - - let conn_2 = SignedConnection::from_connection(cfg.get_connection().await, _2); - let (_, context) = conn_2 - .approve_with_call(context, Some(call), Finalized) - .await - .expect("failed to execute"); - - match context { - ContextAfterUse::Ongoing(_) => panic!("Failed to conclude aggregation"), - ContextAfterUse::Closed(_) => {} - }; - } } Ok(()) }