k-of-n multisignature

The k-of-n multisignature validity predicate authorizes transactions on the basis of k out of n parties approving them. This document targets the encrypted (inner) WASM transactions. Namada does not support multiple signers on wrapper or protocol transactions.


Namada transactions get signed before being delivered to the network. This signature is then checked by the VPs to determine the validity of the transaction. To support multisignature we need to modify the current SignedTxData struct to the following:

fn main() {
pub struct SignedTxData {
    /// The original tx data bytes, if any
    pub data: Option<Vec<u8>>,
    /// The signature is produced on the tx data concatenated with the tx code
    /// and the timestamp.
    pub sig: Vec<(u8, common::Signature)>,

The sig field now holds a vector of tuples where the first element is an 8-bit integer and the second one is a signature. The integer serves as an index to match a specific signature to one of the public keys in the list of accepted ones. This way, we can improve the verification algorithm and check each signature only against the public key at the provided index (), without the need to cycle on all of them which would be .

This means that non-multisig addresses will now be implemented as 1-of-1 multisig accounts (but this difference is transparent to the user).


Since all the addresses will be multisig ones, we will keep using the already available vp_user as the default validity predicate. The only modification required is the signature check which must happen on a set of signatures instead of a single one.

To perform the validity checks, the VP will need to access two types of information:

  1. The multisig threshold
  2. A list of valid signers' public keys

This data defines the requirements of a valid transaction operating on the multisignature address and it will be written in storage when the account is created:

/\$Address/threshold/: u8
/\$Address/pubkeys/: LazyVec<PublicKey>

The LazyVec struct will split all of its elements on different subkeys in storage so that we won't need to load the entire vector of public keys in memory for validation but just the ones pointed by the indexes in the SignedTxData struct.

To verify the correctness of the signatures, this VP will proceed with a two-step verification process:

  1. Check to have enough unique signatures for the given threshold
  2. Check to have enough valid signatures for the given threshold

Step 1 allows us to short-circuit the validation process and avoid unnecessary processing and storage access. Each signature will be validated only against the public key found in the list at the specified index. Step 2 will halt as soon as it retrieves enough valid signatures to match the threshold, meaning that the remaining signatures will not be verified.


The vp introduced in the previous section is available for established addresses. To generate a multisig account we need to modify the InitAccount struct to support multiple public keys and a threshold, as follows:

fn main() {
pub struct InitAccount {
    /// The VP code
    pub vp_code: Vec<u8>,
    /// Multisig threshold for k-of-n
    pub threshold: u8,
    /// Multisig signers' pubkeys to be written into the account's storage. This can be used
    /// for signature verification of transactions for the newly created
    /// account.
    pub pubkeys: Vec<common::PublicKey>

Finally, the tx performs the following writes to storage:

  • The multisig vp
  • The threshold
  • The list of public keys of the signers

Multisignature accounts can also be initialised at genesis time - in this case, the requisite parameters are kept in the genesis file and written to storage during initialisation.

Multisig account init validation

Since the VP of an established account does not get triggered at account creation, no checks will be run on the multisig parameters, meaning that the creator could provide wrong data.

To perform validation at account creation time we could:

  1. Write in storage the addresses together with the public keys to trigger their VPs
  2. Manually trigger the multisig VP even at creation time
  3. Create an internal VP managing the creation of every multisig account

All of these solutions would require the init transaction to become a multisigned one.

Solution 1 actually exhibits a problem: in case the members of the account would like to exclude one of them from the account, the target account could refuse to sign the multisig transaction carrying this modification. At validation time, his private VP will be triggered and, since there's no signature matching his own public key in the transaction, it would reject it effectively preventing the multisig account to operate on itself even with enough signatures to match the threshold. This goes against the principle that a multisig account should be self-sufficient and controlled by its own VP and not those of its members.

Solution 2 would perform just a partial check since the logic of the VP will revolve around the threshold.

Finally, solution 3 would require an internal VP dedicated to the management of multisig addresses' parameters both at creation and modification time. This could implement a logic based on the threshold or a logic requiring a signature by all the members to initialize/modify a multisig account's parameters. The former effectively collapses to the VP of the account itself (making the internal VP redundant), while the latter has the same problem as solution 1.

In the end, we don't implement any of these checks and will leave the responsibility to the signer of the transaction creating the address: in case of an error he can simply submit a new transaction to generate the correct account. On the other side, the participants of a multisig account can refuse to sign transactions if they don't agree on the parameters defining the account itself.

Transaction construction

To craft a multisigned transaction, the involved parties will need to coordinate. More specifically, the transaction will be constructed by one entity which will then distribute it to the signers and collect their signatures: note that the constructing party doesn't necessarily need to be one of the signers. Finally, these signatures will be inserted in the SignedTxData struct so that it can be encrypted, wrapped and submitted to the network.

Namada does not provide a layer to support this process, so the involved parties will need to rely on an external communication mechanism.