Using the Multisig Validator
When installed with the Multisig Validator Module, the starknet modular account
not only can have multiple signers registered but it can also require N of those
signers to agree to sign a transaction. This Multisig feature takes place
offchain and the SmartrAccount
class provides the framework manage several
signers on the same account to sign the a transaction. This is what is shown in
this section.
- Using the Multisig Validator
Note 1: This section assumes the
SmartrAccount
class has been instantiated in thesmartrAccount
variable as shown in Using the modular account from the SDK. It also assumes theCounter
contract that comes with the project has been deploys to thecounterAddress
and theCounterABI
class are available. The03-setup.ts
script that comes with this project ensure those steps are executed.
Note 2: This multi-signer model has some limits: the exchange of the transaction between signers is not managed by the SDK and requires some synchronization between the actors. In addition, the fact the transaction is a regular transaction that involves the account Nonce generated by the network prevents from using it at a large scale. The starknet modular account can be used to implement more advanced models to workaround those 2 issues.
Interacting with the Multisig Validator
The SmartrAccount
class, however, provides more than just the regular
Account
class. It can interact with functions that are part of the module
and not part of the account. In the case of the Multisig Validator, those
functions are:
#![allow(unused)] fn main() { fn get_public_keys(self: @TState) -> Array<felt252>; fn add_public_key(ref self: TState, new_public_key: felt252); fn remove_public_key(ref self: TState, old_public_key: felt252); fn get_threshold(self: @TState) -> u8; fn set_threshold(ref self: TState, new_threshold: u8); }
To execute a function that is part of the module you need:
- to figure out the Multisig Validator module class hash
- to check the module is installed on the account. That is something that is setup at the account deployment time
- to use one of
callOnModule
for view functions orexecuteOnModule
for running transactions on the SmartrAccount.
The sections below dig into the details of these operations.
Getting the stark validator module class hash
This is something we have already done previously. You can use
classHash("MultisigValidator")
after your imported the classHash
function
from @0xknwn/starknet-module
like below:
// file src/03-check-class.ts
import {
classHash,
classNames as moduleClassNames,
} from "@0xknwn/starknet-module";
console.log(
"MultisigValidator class hash:",
classHash(moduleClassNames.MultisigValidator)
);
To execute the script, make sure you have deployed the account in the network and run the following commands:
npx tsc --build
node dist/03-check-class.js
Check the module is installed on the account
The SmartrAccount
provides a method isModule
that can be used to know if
a module is installed with the account.
// file src/03-module-installed.ts
import { SmartrAccount } from "@0xknwn/starknet-modular-account";
import {
classHash,
classNames as moduleClassNames,
} from "@0xknwn/starknet-module";
import { init } from "./03-init";
import { RpcProvider } from "starknet";
const providerURL = "http://127.0.0.1:5050/rpc";
const main = async () => {
const provider = new RpcProvider({ nodeUrl: providerURL });
const { accountAddress, smartrAccountPrivateKey } = await init();
const account = new SmartrAccount(
provider,
accountAddress,
smartrAccountPrivateKey,
undefined,
"1",
"0x3"
);
const isInstalled = await account.isModule(
classHash(moduleClassNames.MultisigValidator)
);
console.log(
"module",
classHash(moduleClassNames.MultisigValidator),
"is installed",
isInstalled
);
};
main()
.then(() => {})
.catch((e) => {
console.warn(e);
});
Transpile and run the script:
npx tsc --build
node dist/03-module-installed.js
Calling views functions in the module
To execute a view function on the module, we must build the argumemt list with
the CallData
class. Thwn we can call the callOnModule
function from
SmartrAccount
with the module class hash, the function name and the calldata
like below:
// file src/03-registered-publickeys.ts
import { SmartrAccount } from "@0xknwn/starknet-modular-account";
import {
MultisigValidatorABI,
classHash as moduleClassHash,
classNames as moduleClassNames,
} from "@0xknwn/starknet-module";
import { init } from "./03-init";
import { CallData, RpcProvider } from "starknet";
const providerURL = "http://127.0.0.1:5050/rpc";
const main = async () => {
const provider = new RpcProvider({ nodeUrl: providerURL });
const { accountAddress, smartrAccountPrivateKey } = await init();
const account = new SmartrAccount(
provider,
accountAddress,
smartrAccountPrivateKey,
undefined,
"1",
"0x3"
);
const moduleCallData = new CallData(MultisigValidatorABI);
const calldata = moduleCallData.compile("get_public_keys", {});
const publickeysList = await account.callOnModule(
moduleClassHash(moduleClassNames.MultisigValidator),
"get_public_keys",
calldata
);
console.log("number of public keys for module", publickeysList.length);
publickeysList.forEach((publickey, idx) => {
console.log("publickey #", idx + 1, `0x${publickey.toString(16)}`);
});
};
main()
.then(() => {})
.catch((e) => {
console.warn(e);
});
Transpile and run the script:
npx tsc --build
node dist/03-registered-publickeys.js
Executing external functions in the module
To execute an external function on the module, we must build the argumemt list
with the CallData
class. Then we can call the executeOnModule
function from
SmartrAccount
with the module class hash, the function name and the calldata
like below. Here we will register a second public key for the same account:
// file src/03-add-publickeys.ts
import {
SmartrAccountABI,
SmartrAccount,
} from "@0xknwn/starknet-modular-account";
import {
MultisigValidatorABI,
classHash as moduleClassHash,
classNames as moduleClassNames,
} from "@0xknwn/starknet-module";
import { init } from "./03-init";
import { CallData, RpcProvider, Signer, hash, type Call } from "starknet";
const providerURL = "http://127.0.0.1:5050/rpc";
const secondAccountPrivateKey = "0x2";
const thirdAccountPrivateKey = "0x3";
const main = async () => {
const provider = new RpcProvider({ nodeUrl: providerURL });
const { accountAddress, smartrAccountPrivateKey } = await init();
const account = new SmartrAccount(
provider,
accountAddress,
smartrAccountPrivateKey,
undefined,
"1",
"0x3"
);
const module_class_hash = moduleClassHash(moduleClassNames.MultisigValidator);
const calls: Call[] = [];
for (const privateKey of [secondAccountPrivateKey, thirdAccountPrivateKey]) {
const signer = new Signer(privateKey);
const publicKey = await signer.getPubKey();
console.log("new account public key", publicKey);
const moduleCallData = new CallData(MultisigValidatorABI);
const moduleCalldata = moduleCallData.compile("add_public_key", {
new_public_key: publicKey,
});
const accountCallData = new CallData(SmartrAccountABI);
const calldata = accountCallData.compile("execute_on_module", {
class_hash: module_class_hash,
call: {
selector: hash.getSelectorFromName("add_public_key"),
to: accountAddress,
calldata: moduleCalldata,
},
});
const call: Call = {
entrypoint: "execute_on_module",
contractAddress: accountAddress,
calldata,
};
calls.push(call);
}
const { transaction_hash } = await account.execute(calls);
const receipt = await account.waitForTransaction(transaction_hash);
console.log("transaction succeeded", receipt.isSuccess());
};
main()
.then(() => {})
.catch((e) => {
console.warn(e);
});
Transpile and run the script:
npx tsc --build
node dist/03-add-publickeys.js
You can re-run the script from the previous example to check the account has two registered public key:
node dist/03-registered-publickeys.js
The output should look like that:
number of public keys for module 3
publickey # 1 0x1ef15c18599971b7beced415a40f0c7deacfd9b0d1819e03d723d8bc943cfca
publickey # 2 0x759ca09377679ecd535a81e83039658bf40959283187c654c5416f439403cf5
publickey # 3 0x411494b501a98abd8262b0da1351e17899a0c4ef23dd2f96fec5ba847310b20
Interacting with a Contract with the new registered key
You now can interact with the SmartrAccount
with your second private key like
below:
// file src/03-execute-tx.ts
import { SmartrAccount } from "@0xknwn/starknet-modular-account";
import { init, CounterABI } from "./03-init";
import { RpcProvider, Contract } from "starknet";
const providerURL = "http://127.0.0.1:5050/rpc";
const secondAccountPrivateKey = "0x2";
const main = async () => {
const provider = new RpcProvider({ nodeUrl: providerURL });
const { accountAddress, counterAddress } = await init();
const account = new SmartrAccount(
provider,
accountAddress,
secondAccountPrivateKey,
undefined,
"1",
"0x3"
);
const counter = new Contract(CounterABI, counterAddress, account);
let currentCounter = await counter.call("get");
console.log("currentCounter", currentCounter);
const call = counter.populate("increment");
const { transaction_hash } = await account.execute(call);
const receipt = await account.waitForTransaction(transaction_hash);
console.log("transaction succeeded", receipt.isSuccess());
currentCounter = await counter.call("get");
console.log("currentCounter", currentCounter);
};
main()
.then(() => {})
.catch((e) => {
console.warn(e);
});
Transpile and run the script:
npx tsc --build
node dist/03-execute-tx-pk2.js
Changing the Account Threshold to 2
By changing the Multisig Validator Threshold to 2, you force transactions to be signed by 2 of the 3 signers of the account. Run a script like below to change the threshold:
// file src/03-increase-threshold.ts
import { SmartrAccount } from "@0xknwn/starknet-modular-account";
import {
MultisigValidatorABI,
classHash as moduleClassHash,
classNames as moduleClassNames,
} from "@0xknwn/starknet-module";
import { init } from "./03-init";
import { CallData, RpcProvider } from "starknet";
const providerURL = "http://127.0.0.1:5050/rpc";
const main = async () => {
const provider = new RpcProvider({ nodeUrl: providerURL });
const { accountAddress, smartrAccountPrivateKey } = await init();
const account = new SmartrAccount(
provider,
accountAddress,
smartrAccountPrivateKey,
undefined,
"1",
"0x3"
);
const moduleCallData = new CallData(MultisigValidatorABI);
const calldata = moduleCallData.compile("set_threshold", {
new_threshold: 2,
});
const { transaction_hash } = await account.executeOnModule(
moduleClassHash(moduleClassNames.MultisigValidator),
"set_threshold",
calldata
);
const receipt = await account.waitForTransaction(transaction_hash);
console.log("transaction succeeded", receipt.isSuccess());
};
main()
.then(() => {})
.catch((e) => {
console.warn(e);
});
Transpile and run the script:
npx tsc --build
node dist/03-increase-threshold.js
You can check the current threshold on the account with the script below:
// file src/04-get-threshold.ts
import { SmartrAccount } from "@0xknwn/starknet-modular-account";
import {
MultisigValidatorABI,
classHash as moduleClassHash,
classNames as moduleClassNames,
} from "@0xknwn/starknet-module";
import { init } from "./03-init";
import { CallData, RpcProvider } from "starknet";
const providerURL = "http://127.0.0.1:5050/rpc";
const main = async () => {
const provider = new RpcProvider({ nodeUrl: providerURL });
const { accountAddress, smartrAccountPrivateKey } = await init();
const account = new SmartrAccount(
provider,
accountAddress,
smartrAccountPrivateKey,
undefined,
"1",
"0x3"
);
const moduleCallData = new CallData(MultisigValidatorABI);
const calldata = await moduleCallData.compile("get_threshold", {});
const threshold = await account.callOnModule(
moduleClassHash(moduleClassNames.MultisigValidator),
"get_threshold",
calldata
);
threshold.forEach((threshold) => console.log("threshold", threshold));
};
main()
.then(() => {})
.catch((e) => {
console.warn(e);
});
Run the script with the command below:
npx tsc --build
node dist/03-get-threshold.js
Checking you can NOT run a transaction with a single signer
The script below executes a transaction with a single signer as it was the case in the previous section:
// file src/03-execute-tx.ts
import { SmartrAccount } from "@0xknwn/starknet-modular-account";
import { init, CounterABI } from "./03-init";
import { RpcProvider, Contract } from "starknet";
const providerURL = "http://127.0.0.1:5050/rpc";
const main = async () => {
const provider = new RpcProvider({ nodeUrl: providerURL });
const { accountAddress, counterAddress, smartrAccountPrivateKey } =
await init();
const account = new SmartrAccount(
provider,
accountAddress,
smartrAccountPrivateKey,
undefined,
"1",
"0x3"
);
const counter = new Contract(CounterABI, counterAddress, account);
let currentCounter = await counter.call("get");
console.log("currentCounter", currentCounter);
const call = counter.populate("increment");
const { transaction_hash } = await account.execute(call);
const receipt = await account.waitForTransaction(transaction_hash);
console.log("transaction succeeded", receipt.isSuccess());
currentCounter = await counter.call("get");
console.log("currentCounter", currentCounter);
};
main()
.then(() => {})
.catch((e) => {
console.warn(e);
});
Make sure you have deployed the account and the counter contract in the network and run the following commands:
npx tsc --build
node dist/03-execute-tx.js
You are now getting an error saying the signature is invalid like below:
Execution failed.
Failure reason: 0x4163636f756e743a20696e76616c6964207369676e6174757265
('Account: invalid signature')
Running a Multiple Signer Transaction
To run a transaction with multiple signers, you need to instantiate several
SmartrAccount
, each one with a different signer. Because you have set the
threshold, you need to instantiate 2 accounts.
Once done, proceed in 3 steps:
- Step 1: generate the transaction details. This requires you create the calls
but also you set some details about it, including: the Fees, the Nonce, the
Version and the Chain. The
SmartrAccount
class uses the provider to get the chain id. To get the other details, you should run theprepareMultisig
that returns the details associated with the transaction. - Step 2: have all the signers generate their part of the signature. The
signMultisig
takes the list of calls and the details you have generated and provides the signature as an array of string - Step 3: Execute the transaction with all the signatures from Step 2. This
could be done by anyone, including one of the account you have already
created. The
executeMultisig
function takes the list of calls, the details and an array that contains all the signatures.
The script below signs the transaction with 2 signers and to run the increment
external function of the Counter
contract. It shows the value of the counter
before and after the call:
// file src/03-execute-tx-multiple-signers.ts
import { SmartrAccount } from "@0xknwn/starknet-modular-account";
import { init, CounterABI } from "./03-init";
import { RpcProvider, Contract, ArraySignatureType } from "starknet";
const providerURL = "http://127.0.0.1:5050/rpc";
const secondSmartrAccountPrivateKey = "0x2";
const main = async () => {
const provider = new RpcProvider({ nodeUrl: providerURL });
const { accountAddress, counterAddress, smartrAccountPrivateKey } =
await init();
const firstAccount = new SmartrAccount(
provider,
accountAddress,
smartrAccountPrivateKey,
undefined,
"1",
"0x3"
);
const secondAccount = new SmartrAccount(
provider,
accountAddress,
secondSmartrAccountPrivateKey,
undefined,
"1",
"0x3"
);
// Before you start check the value of the counter
const counter = new Contract(CounterABI, counterAddress, provider);
let currentCounter = await counter.call("get");
console.log("currentCounter value", currentCounter);
// Step 1: Prepare the transaction and get the details
const call = counter.populate("increment");
const calls = [call];
const detail = await firstAccount.prepareMultisig(calls);
console.log("below are the details assciated with the transaction");
console.log(detail);
// Step 2: Sign the transaction with 2 signers
// (because the threshold on the account is currently 2)
const firstSignature: ArraySignatureType = await firstAccount.signMultisig(
calls,
detail
);
console.log("first signature is", firstSignature);
const secondSignature: ArraySignatureType = await secondAccount.signMultisig(
calls,
detail
);
console.log("second signature is", secondSignature);
// Step 3: Execute the transaction
const { transaction_hash } = await firstAccount.executeMultisig(
calls,
detail,
[...firstSignature, ...secondSignature]
);
const receipt = await firstAccount.waitForTransaction(transaction_hash);
console.log("transaction succeeded", receipt.isSuccess());
// Once finished, check the value of the counter again
currentCounter = await counter.call("get");
console.log("currentCounter value", currentCounter);
};
main()
.then(() => {})
.catch((e) => {
console.warn(e);
});
Transpile and run the script:
npx tsc --build
node dist/03-execute-tx-multiple-signers.js
Reset the threshold to one
As for any transaction, you need to run a multi-signed transaction to reset the
account threshold back to one. The script below build the call to
execute_on_module
and run it with multiple signer:
// file src/03-decrease-threshold.ts
import {
SmartrAccount,
SmartrAccountABI,
} from "@0xknwn/starknet-modular-account";
import {
MultisigValidatorABI,
classHash as moduleClassHash,
classNames as moduleClassNames,
} from "@0xknwn/starknet-module";
import { init } from "./03-init";
import {
CallData,
RpcProvider,
hash,
type Call,
type ArraySignatureType,
} from "starknet";
const providerURL = "http://127.0.0.1:5050/rpc";
const secondSmartrAccountPrivateKey = "0x2";
const main = async () => {
const provider = new RpcProvider({ nodeUrl: providerURL });
const { accountAddress, smartrAccountPrivateKey } = await init();
const firstAccount = new SmartrAccount(
provider,
accountAddress,
smartrAccountPrivateKey,
undefined,
"1",
"0x3"
);
const secondAccount = new SmartrAccount(
provider,
accountAddress,
secondSmartrAccountPrivateKey,
undefined,
"1",
"0x3"
);
// Before you start build the set_threshold call
const moduleCallData = new CallData(MultisigValidatorABI);
const moduleCalldata = moduleCallData.compile("set_threshold", {
new_threshold: 1,
});
const accountCallData = new CallData(SmartrAccountABI);
const calldata = accountCallData.compile("execute_on_module", {
class_hash: moduleClassHash(moduleClassNames.MultisigValidator),
call: {
selector: hash.getSelectorFromName("set_threshold"),
to: accountAddress,
calldata: moduleCalldata,
},
});
const call: Call = {
entrypoint: "execute_on_module",
contractAddress: accountAddress,
calldata,
};
const calls = [call];
// Step 1: Prepare the transaction and get the details
const detail = await firstAccount.prepareMultisig(calls);
// Step 2: Sign the transaction with 2 signers
// (because the threshold on the account is currently 2)
const firstSignature: ArraySignatureType = await firstAccount.signMultisig(
calls,
detail
);
const secondSignature: ArraySignatureType = await secondAccount.signMultisig(
calls,
detail
);
// Step 3: Execute the transaction
const { transaction_hash } = await firstAccount.executeMultisig(
calls,
detail,
[...firstSignature, ...secondSignature]
);
const receipt = await firstAccount.waitForTransaction(transaction_hash);
};
main()
.then(() => {})
.catch((e) => {
console.warn(e);
});
Execute the script with the following commands:
npx tsc --build
node dist/03-decrease-threshold.js
You can check the threshold is back to one:
node dist/03-get-threshold.js
Remove Registered Keys
You can now run transaction with a single signer on the account. The script below shows how to remove 2 public keys from a single call:
// file src/03-remove-publickeys.ts
import {
SmartrAccountABI,
SmartrAccount,
} from "@0xknwn/starknet-modular-account";
import {
MultisigValidatorABI,
classHash as moduleClassHash,
classNames as moduleClassNames,
} from "@0xknwn/starknet-module";
import { init } from "./03-init";
import { CallData, RpcProvider, Signer, hash, type Call } from "starknet";
const providerURL = "http://127.0.0.1:5050/rpc";
const secondAccountPrivateKey = "0x2";
const thirdAccountPrivateKey = "0x3";
const main = async () => {
const provider = new RpcProvider({ nodeUrl: providerURL });
const { accountAddress, smartrAccountPrivateKey } = await init();
const account = new SmartrAccount(
provider,
accountAddress,
smartrAccountPrivateKey,
undefined,
"1",
"0x3"
);
const module_class_hash = moduleClassHash(moduleClassNames.MultisigValidator);
const calls: Call[] = [];
for (const privateKey of [secondAccountPrivateKey, thirdAccountPrivateKey]) {
const signer = new Signer(privateKey);
const publicKey = await signer.getPubKey();
console.log("account public key to remove", publicKey);
const moduleCallData = new CallData(MultisigValidatorABI);
const moduleCalldata = moduleCallData.compile("remove_public_key", {
old_public_key: publicKey,
});
const accountCallData = new CallData(SmartrAccountABI);
const calldata = accountCallData.compile("execute_on_module", {
class_hash: module_class_hash,
call: {
selector: hash.getSelectorFromName("remove_public_key"),
to: accountAddress,
calldata: moduleCalldata,
},
});
const call: Call = {
entrypoint: "execute_on_module",
contractAddress: accountAddress,
calldata,
};
calls.push(call);
}
const { transaction_hash } = await account.execute(calls);
const receipt = await account.waitForTransaction(transaction_hash);
console.log("transaction succeeded", receipt.isSuccess());
};
main()
.then(() => {})
.catch((e) => {
console.warn(e);
});
Transpile and run the script:
npx tsc --build
node dist/03-remove-publickeys.js
You can check the 2 of the 3 public keys have been removed:
node dist/03-registered-publickeys.js