A modular multi-signature implementation for Aleo that provides reusable multisig functionality and an example wallet application.
The multisig engine is implemented in the multisig_core.aleo program. It is built in a way that supports multiple wallets.
In this context, a multisig wallet:
- Is identified by a
wallet_id, which has been chosen to be anaddresstype. - Has a set threshold that specifies the number of signatures required to confirm an operation.
- Has a list of signers that are allowed to sign operations. Signers can be either Aleo addresses or ECDSA (Ethereum) addresses.
- Allows changing the threshold and adding/removing signers via administrative operations.
The wallet_id being an address type makes it easy for other Aleo programs that want to use the multisig functionality to have their own set of signers/threshold directly tied to the program itself.
A program called multisig_wallet.aleo demonstrates how a wallet holding Aleo credits and token_registry.aleo tokens can be built using the functionality provided by multisig_core.aleo.
Creating a wallet is done using the multisig_core.aleo/create_wallet transition. It receives the wallet_id, as well as the initial threshold and set of signers. The threshold and signers can later be updated via administrative operations.
A multisig operation consists of three phases:
- Initializing the signing operation. At this point, a unique identifier called
signing_op_idis chosen by the user, along with ablock_expirationheight (relative to the current block height). This unique identifier is used to identify the operation throughout the signing process. Depending on the usecase this can either be a random value, or derived from inputs that uniquely identify the signing operation. Note thatsigning_op_ids cannot be reused if the operation is currently active or has been successfully executed. However, if an operation expires without being executed, thesigning_op_idcan be reused to restart the process. For example, see calls such asinit_public_transfer(in themultisig_wallet.aleoprogram) orinit_admin_op(in themultisig_core.aleoprogram). Initializing can be done by anyone, whether they are an authorized signer or not. If they are, this will count as the first signature. - Signing by authorized signers until the threshold is met, done by calling the
signtransition (for Aleo signers) orsign_ecdsatransition (for ECDSA signers) on themultisig_core.aleoprogram, passing it thewallet_idandsigning_op_id. ECDSA signatures also require a uniquenonceparameter to prevent signature replay attacks - the signature is computed over the hash of{wallet_id, signing_op_id, nonce}. Signatures must be submitted before the operation expires (i.e., current block height < start height + block expiration). - Finally, once enough signatures have been provided, someone needs to execute the multisig-gated operation. This is done by calling an execute function - for example
exec_public_credit_transfer(inmultisig_wallet.aleo) orexec_admin_op(inmultisig_core.aleo). The execute transition verifies that enough signatures have been provided, and if so executes the operation that was gated by the multisig scheme. Note that the number of signatures required is determined by the current threshold, not the threshold at the time of initialization.
This implementation does not try to hide the signer identities (addresses). A decision was made to prioritize code simplicity and ease of usability as much as possible. Hiding signer identities would likely only be possible using Aleo Records, similar to the approach used in https://github.com/zerosecurehq/zerosecure-multisig-program/tree/main, which uses records to grant signing privileges. By avoiding records the implementation presented here provides support for an unlimited number of signers, and a simplified interface that is more user friendly and require less work when wallet configuration changes.
The multisig_core.aleo program includes a global configuration that must be initialized via the init transition.
Note: The init transition can only be called by the hardcoded DEPLOYER_ADDRESS.
The init transition accepts a guard_create_wallet boolean flag:
false: Open mode. Anyone can create a new multisig wallet.true: Guarded mode. Creating a new wallet requires authorization from themultisig_core.aleowallet itself (the "Guard Wallet").
In guarded mode, to create a wallet with wallet_id:
- Calculate the
signing_op_idby hashing aGuardedCreateWalletOpstruct:You can do this using the helper utility instruct GuardedCreateWalletOp { wallet_id: address, threshold: u8, aleo_signers: [address; 4], ecdsa_signers: [[u8; 20]; 4], }./utils/hash_guarded_create_wallet_op.js(remember tonpm installfirst!). - Authorized signers of
multisig_core.aleomust sign thissigning_op_idusing the standard signing flow. - Once the signature threshold is met,
create_walletcan be called. The program verifies that the creation of this specificwallet_idhas been approved by the Guard Wallet.
The multisig_core.aleo program includes built-in access controls for deployment and upgrades.
- Deployer: The address allowed to deploy the initial version (
edition == 0) and call theinittransition. This address is hardcoded as aconstinmultisig_core.aleo. - Upgrader: The address allowed to deploy future versions (
edition > 0) and calldisallow_upgrades. This address is set via theinittransition and can be changed later by callingset_upgrader_address.
IMPORTANT: The deployer address is hardcoded as const in multisig_core.aleo. You MUST change it to your own address before deployment.
The program includes an upgrade kill switch. By default, upgrades are allowed (initialized to true in init).
The Upgrader can permanently disable future upgrades by calling the disallow_upgrades transition. Once set to false, no further program upgrades can be deployed (enforced by the constructor).
In addition to the multisig_wallet.aleo program, we provide a test_upgrades.aleo program that shows how program upgrades can be gated behind a multisig operation.
leoCLI for program compilation and deployment. The code here is known to work withleo 3.4.0.- Node.js 22+ for running tests
- Local Aleo network (devnet recommended for testing)
Root directory .env:
NETWORK=testnet
PRIVATE_KEY=APrivateKey1zkp8CZNn3yeCseEtxuVPbDCwSyhGW6yZKUYKfgXmcpoGPWH
ENDPOINT=http://localhost:3030
CONSENSUS_VERSION_HEIGHTS=0,1,2,3,4,5,6,7,8,9,10,11
You will want to start a local devnet using this command, using your custom-built leo binary:
leo devnet --storage tmp --clear-storage --snarkos ./tmp/snarkos --snarkos-features test_network --install
leo devnet --storage tmp --clear-storage --snarkos ./tmp/snarkos --snarkos-features test_network --tmux --consensus-heights 0,1,2,3,4,5,6,7,8,9,10,11From inside the programs/multisig_wallet directory, run:
leo deploy --broadcast --consensus-heights 0,1,2,3,4,5,6,7,8,9,10,11 -yInitializing the core program:
After deployment, you must initialize the multisig_core.aleo program. For example, to allow open wallet creation:
leo execute --broadcast --yes multisig_core.aleo/init falseAfter you have built a custom version of the SDK as mentioned in the Prerequisities section above, go to the tests directory run npm install.
You should then be able to run npm test. The test suite uses jest and you can run a specific test instead of the whole suite if desired. For example:
export CONSENSUS_VERSION_HEIGHTS=0,1,2,3,4,5,6,7,8,9,10,11
npm test -- -t 'Cannot create wallet with threshold greater than number of signers'