owned this note
owned this note
Published
Linked with GitHub
# Actor Spec: Multisig
[Back to Master Tracking Doc](https://hackmd.io/LOZjAsz-THelSD5lWqSVlw)
## Contents
[TOC]
## At a Glance
The Multisig actor is a single actor representing a group of Signers. Signers may be external users, other Multisigs, or even the Multisig itself.
## State
```go=
type State struct {
Signers []address.Address
NumApprovalsThreshold uint64
NextTxnID TxnID
InitialBalance abi.TokenAmount
StartEpoch abi.ChainEpoch
UnlockDuration abi.ChainEpoch
PendingTxns cid.Cid
}
```
**`Signers`**: A slice of addresses representing the Multisig's Signers.
* Notes:
* ALL address protocols allowed.
* Each Signer should be unique. No entries within Signers should resolve to other entries.
* Each Signer should have equivalent permissions to `Multisig.Propose` and `Multisig.Approve` within the Multisig.
* Can be modified via `Multisig.AddSigner`, `Multisig.RemoveSigner`, and `Multisig.SwapSigner`
* Invariants:
* `len(Signers) != 0`
**`NumApprovalsThreshold`**: The number of Signers that must Approve a Transaction in order for it to be executed.
* Notes:
* Can be modified via `ChangeNumApprovalsThreshold`
* Invariants:
* `NumApprovalsThreshold <= len(Signers)`
* `NumApprovalsThreshold != 0`
**`NextTxnID`**: The TxnID that will be assigned to the next proposed Transaction.
* Notes:
* `NextTxnID` should be equal to the total number of proposed Transactions in the Multisig's history.
* Invariants:
* `NextTxnID >= 0`
**`InitialBalance`**: Denotes a TokenAmount that is locked in the Multisig until `UnlockDuration` epochs elapse.
* Notes:
* `InitialBalance` is unused unless an `UnlockDuration` is specified on construction.
* `InitialBalance` is unlocked gradually over the `UnlockDuration`
* Invariants:
* `InitialBalance >= 0`
**`StartEpoch`**: Denotes the epoch in which `InitialBalance` was locked for `UnlockDuration`.
* Notes:
* `StartEpoch` is unused unless an `UnlockDuration` is specfied on construction.
* If set, `StartEpoch` is the CurrEpoch in which the Multisig was created.
* Invariants:
* `StartEpoch > 0`
* `StartEpoch <= rt.CurrEpoch()`
**`UnlockDuration`**: Denotes the number of epochs after `StartEpoch` at which point the full amount `InitialBalance` becomes available for use by the Multisig.
* Notes:
* Before `UnlockDuration` elapses, the Multisig should ensure that the correct portion of `InitialBalance` is not spent by any executed proposals.
* Invariants:
* `UnlockDuration >= 0`
**`PendingTxns`**: Before Transactions are executed by the Multisig, they're stored in `PendingTxns`.
* Notes:
* Cid type: HAMT, `map[TxnID]Transaction`
* Fully approved and executed Transactions are deleted from `PendingTxns`
* If `NumApprovalsThreshold` decreases, `PendingTxns` may contain fully-approved but un-executed Transactions.
* These Transactions should be executable via `Multisig.Approve`.
#### Transaction
Resolved from `state.PendingTxns`
```go=
type Transaction struct {
To addr.Address
Value abi.TokenAmount
Method abi.MethodNum
Params []byte
Approved []addr.Address
}
```
**`To`**: The destination of the Send.
* Notes:
* ALL address protocols allowed.
* No restrictions currently exist for this value.
* If `To == Receiver`, the Multisig will `Send` to itself. This is used to access functions like `Multisig.AddSigner`, `Multisig.RemoveSigner`, `Multisig.SwapSigner`, and `Multisig.ChangeNumApprovalsThreshold`.
**`Value`**: The TokenAmount that will be provided via Send.
* Notes:
* Whether the Multisig has `Value` available to Send is checked when the Transaction is fully approved for execution.
* Invariants:
* `Value >= 0`
**`Method`**: The MethodNum that will be invoked via Send.
* Notes:
* No restrictions currently exist for this value.
**`Params`**: The parameters that will be provided to the invoked method via Send.
* Notes:
* No restrictions currently exist for this value.
**`Approved`**: A slice of addresses that have registered approval for this Transaction.
* Notes:
* The address at index 0 is the Proposer of the Transaction. Only this address may Cancel the pending Transaction before its execution.
* Invariants:
* `len(Approved) >= 1`
## Exported Methods
#### 1. Constructor
```go=
func (a Actor) Constructor(rt vmr.Runtime, params *ConstructorParams) *adt.EmptyValue
```
Initializes the Multisig's `State` with a slice of Signers and a threshold approval count before proposals may be passed.
Additionally, if `params.UnlockDuration` is nonzero, any `ValueReceived` vests over a period of time denoted by `params.StartEpoch` and `params.UnlockDuration`.
##### Parameters
```go=
type ConstructorParams struct {
Signers []addr.Address
NumApprovalsThreshold uint64
UnlockDuration abi.ChainEpoch
StartEpoch abi.ChainEpoch
}
```
**`Signers`**: A slice of addresses to initialize as the Multisig's `Signers`
* Requirements:
* `len(Signers) != 0 && len(Signers) <= SignersMax` (256)
* All resolved addresses in `Signers` must be unique.
**`NumApprovalsThreshold`**: The number of `Signers` that must call `Approve` in order to execute a Transaction
* Requirements:
* `NumApprovalsThreshold <= len(Signers)`
* `NumApprovalsThreshold != 0`
**`UnlockDuration`**: If this value is nonzero, specifies that any `ValueReceived` vests linearly over a period of epochs `UnlockDuration`.
* Requirements:
* `UnlockDuration >= 0`
**`StartEpoch`**: If `UnlockDuration` is nonzero, the `CurrentBalance` of the multisig vests linearly over a period of epochs starting at `StartEpoch` and lasting `UnlockDuration` epochs.
##### Failure conditions
* `params.Signers` was empty, or contained duplicate members
* `params.NumApprovalsThreshold` was greater than the number of supplied Signers, or was 0
* `params.UnlockDuration` was negative.
#### 2. Propose
```go=
func (a Actor) Propose(rt vmr.Runtime, params *ProposeParams) *ProposeReturn
```
One of the Multisig's Signers proposes a Transaction to be executed.
* The Caller's approval for the newly pending Transaction is registered as well.
* If the newly pending Transaction already has enough approvals for execution, it is executed.
##### Parameters
```go=
type ProposeParams struct {
To addr.Address
Value abi.TokenAmount
Method abi.MethodNum
Params []byte
}
```
**`To`**: The destination of the Send.
* Notes:
* ALL address protocols allowed.
* No restrictions currently exist for this value.
* If `To == Receiver`, the Multisig will `Send` to itself. This is used to access functions like `Multisig.AddSigner`, `Multisig.RemoveSigner`, `Multisig.SwapSigner`, and `Multisig.ChangeNumApprovalsThreshold`.
**`Value`**: The TokenAmount that will be provided via Send.
* Notes:
* Whether the Multisig has `Value` available to Send is checked when the Transaction is fully approved for execution.
* Requirements:
* `Value >= 0`
**`Method`**: The MethodNum that will be invoked via Send.
* Notes:
* No restrictions currently exist for this value.
**`Params`**: The parameters that will be provided to the invoked method via Send.
* Notes:
* No restrictions currently exist for this value.
##### Return
```go=
type ProposeReturn struct {
TxnID TxnID
Applied bool
Code exitcode.ExitCode
Ret []byte
}
```
**`TxnID`**: The `TxnID` associated with the newly-proposed Transaction.
* Notes:
* Should be equal to `st.NextTxnID` before being incremented.
* Invariants:
* `TxnID >= 0`
**`Applied`**: Whether or not the proposal was executed.
* Notes:
* This should only be the case if `NumApprovalsThreshold == 1`, as it's only possible to have 1 Approval on a newly-proposed Transaction.
**`Code`**: If the Transaction was Applied, `Code` is the ExitCode of the Send.
* Notes:
* This value only matters if `Applied == true`
**`Ret`**: If the Transaction was Applied, `Ret` is the return data of the Send.
* Notes:
* This value only matters if `Applied == true`
##### Failure conditions
* Caller is not a Signable type actor
* Caller is not a Signer in the Multisig
* Specified `Value` to send is negative
#### 3. Approve
```go=
func (a Actor) Approve(rt vmr.Runtime, params *TxnIDParams) *ApproveReturn
```
One of the Multisig's Signers marks their approval for a specific pending Transaction. `Approve` follows roughly these steps:
* If the transaction has sufficient approvals BEFORE the Caller's approval is registered, the Transaction is executed.
* Then, the Caller's approval is registered.
* Finally, if the Transaction has sufficient approvals AFTER the Caller's approval is registered, the Transaction is executed.
In the case that some Transaction `t` has reached the `NumApprovalThreshold` but has not yet been executed, the above order allows a Signer that has already approved `t` to invoke `Approve(t)` to execute the Transaction.
##### Parameters
```go=
type TxnIDParams struct {
ID TxnID
ProposalHash []byte
}
```
**`ID`**: The TxnID of the pending Transaction.
* Requirements:
* Must correspond to an existing Transaction in `st.PendingTxns`
**`ProposalHash`**: OPTIONAL. If not `nil`, this is compared to the hash of the Transaction referenced by `ID`. If the two hashes do not match, execution aborts.
* Notes:
* This is primarily used so the Caller can validate that the TxnID they provide is referencing the expected Transaction. (In case of re-org)
##### Return
```go=
type ApproveReturn struct {
Applied bool
Code exitcode.ExitCode
Ret []byte
}
```
**`Applied`**: Whether or not the proposal was executed.
* Notes:
* If `true`, the other fields in the Return provide information about the execution.
**`Code`**: If the Transaction was Applied, `Code` is the ExitCode of the Send.
* Notes:
* This value only matters if `Applied == true`
**`Ret`**: If the Transaction was Applied, `Ret` is the return data of the Send.
* Notes:
* This value only matters if `Applied == true`
##### Failure conditions
* Caller is not a Signable type actor
* Caller is not a Signer in the Multisig
* Referenced `ID` does not exist in `st.PendingTxns`
* `ProposalHash` is not `nil` AND does not match calculated hash of referenced Transaction.
* Caller has already approved the Transaction AND there are not enough approvals for execution
* Enough approvals exist for execution, but the Multisig does not have the available balance for the Transaction's specified `Value`
#### 4. Cancel
```go=
func (a Actor) Cancel(rt vmr.Runtime, params *TxnIDParams) *adt.EmptyValue
```
Allows the original proposer of a Transaction to cancel its pending status, after which it cannot be approved or executed.
* The "original proposer" of a Transaction is the first `Address` in `Transaction.Approved`
* Once cancelled, the pending Transaction is completely deleted from `st.PendingTxns`.
* Transactions can only be cancelled once.
##### Parameters
```go=
type TxnIDParams struct {
ID TxnID
ProposalHash []byte
}
```
**`ID`**: The TxnID of the pending Transaction.
* Requirements:
* Must correspond to an existing Transaction in `st.PendingTxns`
**`ProposalHash`**: OPTIONAL. If not `nil`, this is compared to the hash of the Transaction referenced by `ID`. If the two hashes do not match, execution aborts.
* Notes:
* This is primarily used so the Caller can validate that the TxnID they provide is referencing the expected Transaction. (In case of re-org)
##### Failure conditions
* Caller is not a Signable type actor
* Caller is not a Signer in the Multisig
* Referenced `ID` does not exist in `st.PendingTxns`
* Caller is not the Proposer of the Transaction in question.
* (`st.PendingTxns[ID].Approved[0] != Caller`)
* `ProposalHash` is not `nil` AND does not match calculated hash of referenced Transaction.
#### 5. AddSigner
```go=
func (a Actor) AddSigner(rt vmr.Runtime, params *AddSignerParams) *adt.EmptyValue
```
Provides the Multisig functionality to add a Signer to `st.Signers`.
* Can only be invoked by the Multisig itself, meaning this is only reachable through an approved and executed Transaction.
* Optionally, may increment `st.NumApprovalsThreshold` by 1.
##### Parameters
```go=
type AddSignerParams struct {
Signer addr.Address
Increase bool
}
```
**`Signer`**: The address that will be added to `st.Signers`.
* Notes:
* ALL address protocols allowed.
* Requirements:
* `Signer` must not exist in `st.Signers` already.
**`Increase`**: If `true`, `st.NumApprovalsThreshold` will be increased by 1.
##### Failure conditions
* Caller is not `Message().Receiver()` (the Multisig itself)
* `params.Signer` is already a Signer
* The additional Signer increases the total number of Signers beyond `SignersMax` (256)
#### 6. RemoveSigner
```go=
func (a Actor) RemoveSigner(rt vmr.Runtime, params *RemoveSignerParams) *adt.EmptyValue
```
Provides the Multisig functionality to remove a Signer from `st.Signers`.
* Can only be invoked by the Multisig itself, meaning this is only reachable through an approved and executed Transaction.
* Optionally, may decrement `st.NumApprovalsThreshold` by 1.
* Only succeeds if the Multisig currently has more than 1 Signer.
##### Parameters
```go=
type RemoveSignerParams struct {
Signer addr.Address
Decrease bool
}
```
**`Signer`**: The address that will be removed from `st.Signers`.
* Notes:
* ALL address protocols allowed.
* Requirements:
* `Signer` must exist in `st.Signers`
**`Decrease`**: If `true`, `st.NumApprovalsThreshold` will be decreased by 1.
* Requirements:
* If removing `Signer` would result in `len(st.Signers) < st.NumApprovalsThreshold`, `Decrease` must be `true`
##### Failure conditions
* Caller is not `Message().Receiver()` (the Multisig itself)
* `params.Signer` is not a Signer
* There is only 1 Signer in the Multisig
* (`len(st.Signers) == 1`)
* `params.Decrease` is `false` AND the removal of `params.Signer` would result in `len(st.Signers) < st.NumApprovalsThreshold`
#### 7. SwapSigner
```go=
func (a Actor) SwapSigner(rt vmr.Runtime, params *SwapSignerParams) *adt.EmptyValue
```
Provides the Multisig functionality to atomically remove one Signer AND add a new Signer.
* Can only be invoked by the Multisig itself, meaning this is only reachable through an approved and executed Transaction.
* Successful execution should leave the number of Signers unchanged.
##### Parameters
```go=
type SwapSignerParams struct {
From addr.Address
To addr.Address
}
```
**`From`**: The address that will be removed from `st.Signers`.
* Notes:
* ALL address protocols allowed.
* Requirements:
* `From` must already be a Signer
**`To`**: The address that will be added to `st.Signers`
* Notes:
* ALL address protocols allowed.
* Requirements:
* `To` must not already be a Signer
##### Failure conditions
* Caller is not `Message().Receiver()` (the Multisig itself)
* `params.From` is not a Signer
* `params.To` is already a Signer
#### 8. ChangeNumApprovalsThreshold
```go=
func (a Actor) ChangeNumApprovalsThreshold(rt vmr.Runtime, params *ChangeNumApprovalsThresholdParams) *adt.EmptyValue
```
Provides the Multisig functionality to adjust the number of Signers needed to pass proposals.
* Can only be invoked by the Multisig itself, meaning this is only reachable through an approved and executed Transaction.
##### Parameters
```go=
type ChangeNumApprovalsThresholdParams struct {
NewThreshold uint64
}
```
**`NewThreshold`**: The new `NumApprovalsThreshold`
* Requirements:
* `NewThreshold != 0`
* `NewThreshold <= len(st.Signers)`
##### Failure conditions
* Caller is not `Message().Receiver()` (the Multisig itself)
* `params.NewThreshold == 0`
* `params.NewThreshold > len(st.Signers)`
## Open Questions
* If a Signer is added, should they immediately be able to Approve prior `PendingTxns`?
* If a Signer is removed, should the `PendingTxns` they approved reflect this by removing the Signer from `Approved`?
* If `NumApprovalsThreshold` is changed, should prior `PendingTxns` use the updated value?
* Should it be possible to `Propose` a transaction, even if it sends more value than is currently available to the multisig?
* Should it be possible to `Approve` a transaction, even if it sends more value than is currently available to the multisig?
* If a proposal meets the approval threshold (but has not yet been executed), should the proposal be able to be cancelled?
* Is Multisig.SwapSigner intended specifically for the scenario where a Signer wishes to use a different public key?
* Should Multisig.ChangeNumApprovalsThreshold fail if the new threshold is the same as the old?
## Questions
* Given that actor state is loaded all-at-once, is it possible that this object may get too large to load (with arbitrarily-large `Signers`)?
* anorth: Yes.
* Does a `Signer` need to exist on chain before being added to a multisig's `Signers`?
* anorth: No.
* Can a multisig be one of its own `Signers`?
* anorth: Yes.
* If the original proposer is removed from the multisig, can the proposal be cancelled?
* anorth: No (but convince me we should change that)
* Can a multisig send value without executing a method? What would `Method` be in this case?
* anorth: method number 0 is a pure value send