Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multisignature specs #680

Merged
merged 5 commits into from
Dec 5, 2022
Merged
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 113 additions & 2 deletions documentation/specs/src/base-ledger/multisignature.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,114 @@
## k-of-n multisignature
# k-of-n multisignature

The k-of-n multisignature validity predicate authorises transactions on the basis of k out of n parties approving them.
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 wasm transactions: at the moment there's no support for multisignature on wrapper or protocol transactions.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do not need multisignatures for wrapper or protocol transactions, as far as I am aware


## Protocol

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 extend the current `SignedTxData` struct to the following:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

either:

  • there should be one VP for all accounts, which is a multisignature (and regular accounts are just 1-of-1), or
  • we should use a different struct for the two different VPs

It looks like you're proposing the latter, right? wdyt about the former? it does reduce the number of VPs by one...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the idea of a single VP should work and we could use vp_user also for multisig accounts


```rust
pub enum Signature {
Sig(common::Signature),
MultiSig(Vec<(u8, common::Signature))
}

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: Signature,
}
```

The `MultiSig` variant 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 (linear in time complexity), without the need to cycle on all of them which would be $\mathcal{O}(n^2)$.

## VPs

To support multisig we provide a new generic `vp_multisig` wasm validity predicate that will allow for arbitrary actions on the account as long as the signatures are valid. Note that these modifications can affect the multisig account parameters themselves.

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/multisig/threshold/: u8
/\$Address/multisig/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, these VPs will proceed with a three-step verification process:

1. Check that the type of the signature is `MultiSig`
2. Check to have enough **unique** signatures for the given threshold
3. Check to have enough **valid** signatures for the given threshold

Steps 1 and 2 allow 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 3 will halt as soon as it retrieves enough valid signatures to match the threshold, meaning that the remaining signatures will not be verified.

In the transaction initiating the established address, the submitter will be able to provide a custom VP if the provided one doesn't impose the desired constraints.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, I think we won't support non-whitelisted VPs in Namada initially though, omit this for now?


## Addresses

The multisig vp introduced in the previous section is available for `established` addresses. To generate a multisig account we provide a new `tx_init_multisig_account` wasm transaction to be used instead of the already available `tx_init_account`. A multisig account can be created by anyone and the creator is responsible for providing the correct data, represented by the following struct:

```rust
pub struct InitMultiSigAccount {
/// The VP code
pub vp_code: Vec<u8>,
cwgoes marked this conversation as resolved.
Show resolved Hide resolved
/// Multisig threshold for k-of-n
pub threshold: u8,
/// Multisig signers' pubkeys
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

`Internal` addresses may want a multi-signature scheme on top of their validation process as well. Among the internal ones, `PGF` will require multisignature for its council (see the [relative](../economics/public-goods-funding.md) spec). The storage data necessary for the correct working of the multisig for an internal address are written in the genesis file: these keys can be later modified through governance.

`Implicit` addresses are not generated by a transaction and, therefore, are not suitable for a multisignature scheme since there would be no way to properly construct them. More specifically, an implicit address doesn't allow for:

- A custom, modifiable VP
- An initial transaction to be used as an initializer for the relevant data

## Multisig account init validation
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should be able to skip all this by having a fixed code for a k-of-n multisig VP and just different data

Copy link
Collaborator Author

@grarco grarco Nov 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah in the end we won't apply any kind of validation (actually not even in case the VP was a custom one)


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.

## Replay protection

The [replay protection](./replay-protection.md) mechanism of Namada will prevent third-party malicious users from replaying a transaction having a multisig account as the source. This mechanism, though, is not enough to completely protect multisigned transactions: in this case, in fact, the threat could come from the members of the account themselves.

With the current hash-based replay protection mechanism, once the transaction has been executed, its hash gets stored to prevent a replay. The issue, with a multisig transaction, is that the same transaction with a different set or amount of signatures would have a different hash, meaning that it can be replayed.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it might be worth adding a counter to multisig for this reason

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we hash the inner-inner transaction (what the multisig signers sign over) and replay-protect with that?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's a better idea! We can use the hash of an unsigned tx, which we can reconstruct from a signed tx (we already do this for sig verification)


There's no way to mitigate this scenario with the current implementation, so the members of a multisignature account will be responsible for behaving honestly towards each other and, possibly, punish malicious users by excluding them from the account.