This is a guide for converting an existing account on Concordium into a multi-sig account and using it to send multi-sig transactions to contracts via scripts.
- Export two account key files A and B from the browser wallet. This would be done by two independent people A and B.
Checkout the guide Concordium Wallet for Web
on how to export key files:
https://developer.concordium.software/en/mainnet/net/guides/export-key.html
The exported key file of Account A (e.g. 4jxvYasaPncfmCFCLZCvuL5cZuvR5HAQezCHZH7ZA7AGsRYpix.export
) should have a content similar to:
{
"type": "concordium-browser-wallet-account",
"v": 0,
"environment": "testnet",
"value": {
"accountKeys": {
"keys": {
"0": {
"keys": {
"0": {
"signKey": "751c9...a38ae",
"verifyKey": "d3e85058f11336715c0ed479a22bd2d04f17366e8da968f7445c237faec1f753"
}
},
"threshold": 1
}
},
"threshold": 1
},
"credentials": {
"0": "97f325c9f86066ab0c80ff879c21629eb67818841940869308d6a72886d18f8668e62e43ad228fdcbda245d0722454df"
},
"address": "4jxvYasaPncfmCFCLZCvuL5cZuvR5HAQezCHZH7ZA7AGsRYpix"
}
}
- Follow this guide to update the account keys of account A.
https://gist.github.com/limemloh/8c0c55f67cf5a83ac7cc21cb646e65c1
The update-keys.json
file in the last section of above guide should look as follows:
{
"keys": {
"0": {
"verifyKey": "<Hex encoding of public key Account A>"
},
"1": {
"verifyKey": "<Hex encoding of public key Account B>"
}
},
"threshold": 2
}
Note: The threshold was set to 2 in the above example. Meaning account A, will be a 2 out of 2 multi sig account from now on.
We will modify the following deployment script as an example:
- Make sure you hardcode the transaction
expiry
time in the script below to a reasonable value so that person A and B sign and send the transaction with the sameexpiry
time.
use concordium_rust_sdk::{
common::types::{CredentialIndex, KeyIndex, Signature, TransactionTime}
};
...
pub async fn update_contract(
&mut self,
update_payload: UpdateContractPayload,
energy: Option<GivenEnergy>,
expiry: Option<TransactionTime>,
) -> Result<(TransactionHash, BlockItemSummary), Error> {
println!("\nUpdating contract....");
let nonce = self.get_nonce(self.key.address).await?;
if !nonce.all_final {
bail!("Nonce not final")
}
let payload = transactions::Payload::Update {
payload: update_payload,
};
// Hadcode a reasonable expiry time here.
let expiry =
expiry.unwrap_or_else(|| TransactionTime::from_seconds((1712243397 + 100000) as u64));
let energy = energy.unwrap_or(GivenEnergy::Absolute(Energy { energy: 50000 }));
let mut tx = transactions::send::make_and_sign_transaction(
&*self.key,
self.key.address,
nonce.nonce,
expiry,
energy,
payload,
);
println!(
"Share this signature to your second party: {:?}",
tx.signature.signatures
);
let tx_local = tx.signature.signatures.entry(CredentialIndex { index: 0 });
let mut other_signature = BTreeMap::new();
other_signature.insert(
KeyIndex::from(1),
Signature {
sig: vec![207, 100, 165, 247, 44, 40, 202, 53, 255, 97, 99, 253, 50, 97, 39, 63, 172, 69, 250, 113, 10, 20, 97, 225, 208, 254, 252, 84, 30, 195, 241, 82, 142, 58, 172, 172, 232, 22, 135, 90, 125, 107, 186, 144, 146, 68, 16, 198, 190, 27, 141, 77, 58, 225, 138, 251, 161, 217, 111, 253, 103, 34, 114, 15],
},
);
tx_local.and_modify(|x| x.append(&mut other_signature));
let bi = transactions::BlockItem::AccountTransaction(tx);
bail!(Error::msg(
"do not send transaction first; collect just the signature; give signature to second party; update above `other_signature`, and then remove this error to send the transaction on chain"
));
...
}
-
Person B generates a modified account key file (named
Account_A_modified.export
):-
Get the Account A key file (without the actual keys in it)
-
Add your Account B keys into the file.
-
Meaning the string {"signKey":"751c.....a38ae","verifyKey":"d3e85058f11336715c0ed479a22bd2d04f17366e8da968f7445c237faec1f753"}
part in the file should be replaced with the keys from Account B.
{
"type": "concordium-browser-wallet-account",
"v": 0,
"environment": "testnet",
"value": {
"accountKeys": {
"keys": {
"0": {
"keys": {
"0": {
"signKey": <Account_B_Signing_Key>,
"verifyKey": <Account_B_Public_Key>
}
},
"threshold": 1
}
},
"threshold": 1
},
"credentials": {
"0": "97f325c9f86066ab0c80ff879c21629eb67818841940869308d6a72886d18f8668e62e43ad228fdcbda245d0722454df"
},
"address": "4jxvYasaPncfmCFCLZCvuL5cZuvR5HAQezCHZH7ZA7AGsRYpix"
}
}
- Person B runs the script with its modified account key files to generate a signature:
e.g.
cargo run register --node http://node.testnet.concordium.com:20000 --account ./4jxvYasaPncfmCFCLZCvuL5cZuvR5HAQezCHZH7ZA7AGsRYpix_modified.export --registry "<7281,0>" --contract "<7373,0>" --contract "<7283,0>"
This will print the signature into the terminal. Communicate the signature to person A.
-
Person A runs the script with the account key file A (as exported from the browser wallet) to sign and send the transaction on-chain:
-
Update the
other_signature
to the signature provided by person B. -
Remove the error part from the script
-
bail!(Error::msg(
"do not send transaction; first collect just the signature; give signature to second party; update above `other_signature`, and then remove this error to send the transaction on chain"
));
- Run the script as normal to sign and send the transaction on-chain:
e.g.
cargo run register --node http://node.testnet.concordium.com:20000 --account ./4jxvYasaPncfmCFCLZCvuL5cZuvR5HAQezCHZH7ZA7AGsRYpix.export --registry "<7281,0>" --contract "<7373,0>" --contract "<7283,0>"
can