---
# System prepended metadata

title: Capability based signature verification for on-chain node management

---

                # Capability based signature verification for on-chain node management

:::info
:bulb: This document is prepared for internal discussion.
:::

:small_blue_diamond:Version: v2.0.0

:small_blue_diamond:Alias: Staking V2

:small_blue_diamond:Relevant discussions: 

- https://github.com/hoprnet/hoprnet/issues/4670
- https://github.com/hoprnet/hoprnet/issues/4699
- https://github.com/hoprnet/hoprnet/issues/4732
- https://docs.google.com/document/d/1CZD0F-zJaO2vYzCcm3J_o-eiOqu_qkv7V_jO-R4CDZ8
- https://github.com/hoprnet/hoprnet/issues/4758


## :beginner: Basic concepts

### Stakeholders
- **Origin of funds**: An Ethereum address that owns some HOPR/xHOPR/wxHOPR/mHOPR tokens, before those tokens enter the HOPR operation ecosystem. E.g. CEX, HODLr of HOPR, investor's multisig.
- **Node runner**: A participant who runs a HOPR node in the network. A "node runner" has access to the "admin key" (see below) associated to the running nodes. 

### Keys and signatures used in HOPR nodes
- **Admin key**: Private key of an Ethereum address that manages its HOPR/xHOPR/wxHOPR/mHOPR tokens for node running. It can enable/disable node's capability of doing on-chain updates. Admin key can be the same as the private key of the "origin of funds", or a dedicated address appointed by the origin of funds. The address of an admin key can be associated with multiple nodes. Admin key does not necessarily need to hold native tokens. It's primary usage is to produce signatures. <span style="color:red">Admin key must be different from Chain key.</span>
- **Chain key**: A private key (secp256k1) managed by (or stored locally in) the HOPR node for on-chain actions, such as announce its multiaddress, redeem acknowledged tickets, update channel status. <span style="color:red">Chain key must be different from Admin key.</span>
- **Packet key**: A private key (Ed25519)[^robert_proposal] managed by (or stored locally in) the HOPR node for creating and transforming packet headers. This key is not used for onchain signature verification due to its high gas cost [^ed25519_implementation][^eip665][^ethresearch].
- **Signatures**: There are two types of signatures relevant to this proposal: The one created by the "chain key", and the one created by its associated "admin key".

### Tri-contract design

- **CapabilityManagement contract**: A smart contract that stores the co-signing requirements per capability. E.g. "Close channel" requires both signatures from `BOTH` chain key and admin key, "Open channel" requires only signatures from the chain key. Capabilities can be set for i) all the channels; ii) all the channels with stake above a certain amount; iii) one particular channel ID; iv) one particular channel ID when its stake is above a certain amount. A collection of capability definition can be set 
- **Staking contract**: An ~~upgradable~~ smart contract that holds wxHOPR tokens for admin keys. Assets in this contract can be transferred to/from "HoprChannels" contract for channel opening/closing respectively.
- **HoprChannels contract**: A smart contract that stores HOPR's payment channel graph and holds all the stakes in payment channels.

Here below is a schematic representation of three contracts. 

![](https://i.imgur.com/7gBjhmU.png)
<p style="text-align: center;">⏶ Diagram of tri-contract architecture</p>

Quick recap on some concepts/terminologies:
- **Payment channel**: In HOPR protocol, payment channels reflects the state of economic incentives reserved for destination nodes to relay packets for source nodes in the decentralized messaging network. Payment channels are unidirectional.
- **Stake**: The stake of a payment channel is the maximum reward that a destination node can collect with its acknowledged tickets after relaying packets
- **Environment**: A pure DevOps term which refers to a specific stage in the software development life cycle. There are four environments used in the project.
  - local: Represents the anvil localhost deployment
  - development: Represents the nodes that will be used in PR targeted to master
  - staging: Represents the nodes that will be used in PR targeted to release/
  - production: Represents the nodes that will be used in real scenario.
- **Network**: “Network” was previously called “environment”. Each network has an internal release name, e.g. “monte_rosa”, “monte_rosa_2_0”, and a unique HoprChannels smart contract. The change of the terminology is proposed in [#4835](https://github.com/hoprnet/hoprnet/issues/4835).
- **Chain**: Blockchain that is used by the network, e.g. "Gnosis chain", "Ethereum mainnet".

## :dart: Design rationales 

1. Each HOPR node has a locally stored or has easy aceess to one "chain key" and one "packet key"
2. An "admin key" is associated with a HOPR node, which sets node's capabilities of performing on-chain actions.
4. HOPR node should only start when some HOPR tokens have been staked.
5. HOPR node can announce its multiaddress, redeem acknowledged tickets, update channel status.
6. HOPR tokens staked by the "admin key" should not be at risk even if the the "chain key" is compromised.
7. Signatures become the center piece of on-chain execution. In an extreme case, HOPR nodes can be run without any native token and thus create business opportunities for other apps in the HOPR ecosystem. (e.g. Exit node gets reward when broadcasting signatures)
8. Slashing is not in the current scope of design.
9. Contracts that hold asset or contain critical logic are not upgradable.

[^robert_proposal]: [issue 4732](https://github.com/hoprnet/hoprnet/issues/4732)
[^ed25519_implementation]: An [Ed25519 implementation in Solidity](https://github.com/chengwenxi/Ed25519/blob/main/contracts/libraries/Ed25519.sol) "Requires up to 1'250'000 gas for one scalar multiplication."
[^eip665]: [EIP-665: Add precompiled contract for Ed25519 signature verification](https://eips.ethereum.org/EIPS/eip-665)
[^ethresearch]: [Ethereum research: Verify ed25519 signatures cheaply on Eth using ZK-Snarks
](https://ethresear.ch/t/verify-ed25519-signatures-cheaply-on-eth-using-zk-snarks/13139)

## :mag: Threat model
1. User removes persisted storage in a restart and thus lose the "chain key" and/or "packet key".
2. Due to maloperation, attacker get a copy of "chain key" and/or "packet key"; or have the access to sign arbitrary messages with "chain key" and/or "packet key".
3. Loss of admin key cannot be well mitigated with the proposed setup, because it results in losing control of assets in the Staking contract and losing the direct control of asset allocation to node running.
4. Losing controle of both "admin key" and "chain key" at the same time is out of scope.

[![](https://i.imgur.com/R1cQJsn.png)](https://i.imgur.com/R1cQJsn.png) 
<p style="text-align: center;">⏶ Table with some under-attack scearios. Click to zoom.</p>


## :triangular_ruler: Features

### Multi-network support
This staking v2 design can be used for multiple networks [^network_environment].
[^network_environment]: "Network" was previously called "environment", e.g. "monte_rosa", "monte_rosa_2_0". The change of the terminology is proposed in [#4835](https://github.com/hoprnet/hoprnet/issues/4835).

Relationship between the Staking contract and a CapabilityManagement contract is **one-to-one**.
Relationship between a CapabilityManagement contract and a HoprChannels contract is **one-to-many**.

![](https://i.imgur.com/frdHE0B.png)
<p style="text-align: center;">⏶ Relationship diagram of contracts</p>

There is one **HoprChannels contract** per network.

On a given chain such as "Gnosis chain", admin keys only need to stake once in the corresponding **Staking contract**. Consequently, a single contract can serve multiple networks on the same chain, whether it be "staging" or "production", of the same chain, like the Gnosis chain. This approach allows admin keys to avoid the need to stake for each individual network.

The **CapabilityManagement contract** is the center place where all the capabilities are stored. 
Admin keys can define multiple sets of capability rules. Each set of rules contains a complete settings on capabilities of performing actions mentioned below. 
Admin keys can also define the set of capability rules applied for each network.
This makes it easy for admin keys to apply the same set of capability management rules across multiple networks.

Each capability possibly results in one of the three `ApprovalLevel`: 
- `ApprovalLevel.CHAIN_KEY` requires a signature from or the function caller to be the chain key
- `ApprovalLevel.ADMIN_KEY`requires a signature from or the function caller to be the admin key
- `ApprovalLevel.EITHER`requires a signaturefrom or the function caller to be either the chain key or the admin key
- `ApprovalLevel.BOTH` requires two signatures from both the chain key or the admin key, or one of the signature and the function caller to be the other required one.

List of actions that need capability configuration.

- **Set capability**: Configure the capability rule set for a node in a network. The default capability is `Approval.ADMIN_KEY`.
- **Announce**: Defined by `CAPABILITY_ANNOUNCE` per node. The default capablity is `ApprovalLevel.BOTH`
- **Open channel**: Open a channel for any source to any destination. Defined by `CAPABILITY_OPEN_CHANNEL` per node. The default capability is `ApprovalLevel.EITHER`.
- **Bump channel**: Defined by `CAPABILITY_BUMP_CHANNEL` per node. The default capability is `ApprovalLevel.EITHER`
- **Fund channel**: <span style="color:red">*No capability defined for this.* Any account can do this for any channel.</span>
- **Initiate (outgoing) channel closure**: Defined by `CAPABILITY_INITIATE_CHANNEL_CLOSURE` per node. The default capability is `ApprovalLevel.EITHER`
- **Finalize (outgoing) channel closure**: Defined by `CAPABILITY_FINALIZE_CHANNEL_CLOSURE` per node. The default capability is `ApprovalLevel.EITHER`
- **Close (incoming) channel**: Defined by `CAPABILITY_CLOSE_CHANNEL` per node. The default capability is `ApprovalLevel.EITHER`
- **Force close channel** Defined by `CAPABILITY_FORCE_CHANNEL_CLOSURE` per node. The default capability is `ApprovalLevel.ADMIN` <span style="color:red">This approval level cannot be changed.</span>
- **Redeem ticket**: Defined by `CAPABILITY_REDEEM_TICKET` per node. The default capability is `ApprovalLevel.EITHER`


### Channel-driven asset flow

Admin key can send wxHOPR tokens from origin of funds. It's stored as "Admin Key disposable token" in the staking contract. 
Before node starts to open channels, admin key must allocate its deposit to "node disposable token". The allocated amount got deducted from `adminKeyDisposableTokenAmount`. 

[![](https://i.imgur.com/3RJq9mg.png)](https://i.imgur.com/3RJq9mg.png) 
<p style="text-align: center;">⏶ Diagram of asset flow related to channel update. Click to zoom.</p>

wxHOPR tokens disposable for channel operation is not reserved for a specific channel. It works as a pool for all the channels. Depending on the capability configuration, a combination of valid signatures from the chain key (most likely managed by the node's strategy) and the admin key (owned by the node runner) can pull/push disposable asset from/to the Staking contract to/from the HoprChannel contract.

When openning channels, HoprChannels contracts can pull tokens from Staking contract to HoprChannels contract as an `operator`[^erc777_intro]. The amount of stake (into the channel) gets deducted from `nodeDisposableTokenAmount` according to the `operatorData`[^erc777_api].
Similarly, when closing channels or redeem tickets, HoprChannels contracts pushes tokens from HoprChannels contract to Staking contract. 

Unlike the the current implementation, stake is strictly managed per channel and no tokens are sent to the channel in the opposite direction.

[^erc777_intro]: [ERC777 operators](https://docs.openzeppelin.com/contracts/2.x/erc777) are special accounts (usually other smart contracts) that will be able to transfer tokens on behalf of their holders.
[^erc777_api]: [ERC777 operatorSend method](https://docs.openzeppelin.com/contracts/2.x/api/token/erc777#IERC777-operatorSend-address-address-uint256-bytes-bytes-)

### Speed-bump in moving asset between contracts

When redeeming ticket redemption, opening/(re-)fundig channels and closing channels, wxHOPR tokens are sent between Staking contract and HoprChannels contract. 

To prevent unintended transfers of a large amount of assets, the CapabilityManagement contract now includes a configurable speed-bump that limits the amount of tokens that can be transferred at once. This measure acts as a safeguard to hinder the accidental transfer of a substantial portion of assets.

The cap is an absolute number.

### Mandatory delay in closing channel with admin key

A new function of `forceChannelClosure` is introduced so that the admin key can close channels on behalf of chain key, when the node runner loses control of its chain key.

An incoming channel can be closed immediately channel, whilst for an outgoing channel, the function has to be called twice by the admin key with a minimum waiting interval between the two calls.

The waiting interval is set globally per network.

However, this functionality seems redundant if `ApprovalLevel.EITHER` is sent for "Initiate (outgoing) channel closure", "Finalize (outgoing) channel closure" and "Close (incoming) channel"

### Global pause for asset withdrawal

Withdrawal of operational HOPR tokens (in HoprChannels contract and Staking contract) can be paused globally by the DAO (or another appropriate account, e.g. HOPR Association, Dev team) when a large-scale malicious attack is detected.

### ~~Possible extension for DAO~~

~~To enable node runners to contribute their unused liquidity towards community events, it is possible to add a new field to the Staking contract specifically for DAO allocation.~~

### Gasless transaction
Signature is at the center of capability validation. Admin key and chain key don't necessarily need to own native tokens.

### Allow to use xHOPR staking contract
- Deposit: Allow both xHOPR and wxHOPR. Internally wrap to wxHOPR.
- Withdraw: Allow both. User must specifiy it. 

## :feet: User story

### Establish 2FA
1. Node runner sends their wxHOPR tokens to the "Staking contract". If using `send` method, address specified in `userData` is the Ethereum address of the admin key. If nothing is specified or `transfer` method is used, then the `msg.sender` is the admin key. 
2. When running a node, node creates a Ed25519 asymmetric key-pair locally (as "identity"), derives its "peer ID". 
3. Admin key allocates some of their available wxHOPR tokens in the "Staking contract" to the node address.
4. HOPR node signs an EIP-712 typed structured data[^eip712] `Node` (see Demo section) and gets a signature.
5. Admin key signs an EIP-712 typed structured data[^eip712] `Node` (see Demo section) and gets a signature.
6. Any account can take those two signatures from step 4 & 6 and call `onStart2FA`. Valid `Node` address pairs are stored in "CapabilityManagement contract" as additions for a network. 
If the function caller (`msg.sender`) is one of the "admin key" and "chain key", then only one signature of the other key is required.

[^eip712]: [EIP-712 typed structured data signing](https://eips.ethereum.org/EIPS/eip-712)


HOPR node starts. Indexer starts. Create listener on connector.

In case of a restart where local identity has been created,
- check if `nodeDisposableTokenAmount > 0` in the "Staking contract". If not, wait until step 3 is completed
- check if there's an admin key address associated in the "CapabilityManagement" contract. If not, wait until step 4, 5, 6 are completed.

### Network registry
Can be removed or repurposed for PRNs [^repurpose_NR].
[^repurpose_NR]: Repurpose of Network Registry for PRN (public relay node) only [#3807](https://github.com/hoprnet/hoprnet/issues/3807)

After the "establish 2FA" process and a fully synced payment channel graph, the link between
- node's peer ID
- node's address
- node's stake in all the outgoing channels
- node's disposable wxHOPR token amount
are known to the node. 

With a known `NETWORK_REGISTRY_STAKE_THRESHOLD`, node should know which peers to establish connection with.

### Configure capability rule sets

Define the approval level and the threshold value for each action. E.g. until 100 wxHOPR token, opening a channel only requires a signature from "chain key"; whilst above 100 wxHOPR until 5000 wxHOPR token, opening a channel requires two signatures from both "admin key" and "chain key".

A set of rules is completed when capabilities for all the actions are defined. 

Admin key sets which set of rules to be used for each network (HoprChannels contract).

### Strategy in operation

Taking "promiscuous" strategy as an example, the chain key broadcast a `openChannel` transaction with the wxHOPR amount as stake. 
HoprChannels contract first validates the capability, i.e. the chain key is allowed to open the said channel. 
Next, the HoprChannels contract verifies that the "node disposable amount" in the Staking contract is sufficient to cover the prospective stake. 
Once the validation is successful, the HoprChannels contract `send`s the wxHOPR tokens from the Staking contract to itself and opens the channel.

## :question: Question:
1. How should the "node runner" to be notified that its signature is awaited? 
2. Should node stores all the sigatures created with the local private key in the database? Any clean up process?
3. Should all the signatures have an additional field `validUntil` (timestamp) so that signatures expires when timeout.

## :wrench: Specs

### CapabilityLib
1. `ApprovalLevel` enum definition `{CHAIN_KEY, ADMIN_KEY, EITHER, BOTH}`
2. `validateSignatures`: Validate signature with `ECDSA.tryRecover` and returns the validation result in bool.

### HoprChannels
Extend the existing HoprChannels contract with
1. Additional constructor argument `_CapabilityManagement` that accepts the address of `ICapabilityManagement` interface.
2. Each public/external channel operation function has an additional argument for an array of signatures.
3. HoprChannels is an operator of wxHOPR token.
4. Modify the token transfer logic so it acts as an ERC777 operator.


#### Additional events in HoprChannels
- `CapabilityManagementSet`

### ICapabilityManagement
Contains interface for the following function(s)
- `onCapabilityValidation()`

### CapabilityManagement
1. inherit `ICapabilityManagement` interface
2. `CapabilityApproval` structure definition: It contains a `uint256` of threshold and `ApprovalLevel`
3. MANAGER: Special role. Use `AccessControl`  of contracts are implemented with. Manager role can be granted to multiple addresses.
4. `newtorkAddressByName`: Stores addresses of HoprChannels in a mapping with its network name as hash, e.g. `keccak256(bytes("monte_rosa_2_0"))`
5. `managerAddNetwork` function: Add a pair of network address and network name to the capability management contract.
6. `managerAddNetwork` function: Remove a pair of network address and network name to the capability management contract.
7. `getNetworkNameHash` function: Return a network name hash with the network name string.
8. capability hash: Identifiers for actions. It's defined with keccak256 hash of capability strings, e.g. `CAPABILITY_SET_CAPABILITY`, `CAPABILITY_ANNOUNCE` (see "Demo" section)
9. `capabilityRuleSets`: KV pair of "capability rule set ID" and `mapping(bytes32=>mapping(bytes32=>CapabilityApproval[]))`. The value is a mapping of "Capability hash" to "channel ID (if 0x0, then apply to all the channels)" and to `Array<CapabilityApproval>`
10. `getCapabilityRuleSetId` function: Return a unique hash as an identifier for CapabilityRuleSet `keccak256(abi.encode(adminKeyAddress,chainId, capabilityRuleSetIndex))`
11. `ownedCapabilityRuleSets`: Stores the ID of `capabilityRuleSets` created by admin keys in `EnumerableMap`.
12. `ruleSetApplied`: Stores the capabiltiy rule set ID applied in a network for an admin key. It's a mapping of "ethereum address of admin key" to "HoprChannels address of a network" to "Capability rule set ID". NB: An admin key can apply a set of capability rules created by anyone, as long as the rule set exists. This enables sharing configuration.
13. `applyRuleSet`: Admin key set which capability rule set ID to be applied in a newtork. It updates `ruleSetApplied` storage and emits `CapabilityRuleSetApplied` event
14. `createCapabilitySet` function: Create a new capability set and set capability approval for an action (and a channel). Function emits `CapabilityRuleSetCreated`
15. `updateCapabilitySet` function: Capability rule owner updates a capability for a given `capabilityRuleSetId`. Funciton emits `CapabilityRuleSetUpdated` event
16. `onCapabilityValidation` function: Can only be called by registered HoprChannels addresses. Returns the result of capability check in bool.
17. `onSignatureValidation` function: Internal function that verifies signatures provided by HoprChannels contract.
18. `nodes`: Store chain key and admin key association in `mapping(bytes32=>Node)`, where `bytes32` is a nodeId defined with `keccak256(abi.encode(chainId,adminKeyAddress,chainKeyAddress))`
19. `onStart2FA` function: Register a pair of admin key and chain key as a `Node` sturcture in `nodes`, emits `KeysAssociated` event.
20. `getNodeIdByAdminKeyAndNetwork` function: Get node ID(s) with an admin key Ethereum address and a network address.
21. `getNodeIdByChainKeyAndNetwork` function: Get node ID(s) with an chain key Ethereum address and a network address.
22. `reclaimErc20Tokens` function: MANAGER reclaims ERC20 fungible tokens accidentally sent to the contract.
23. `reclaimErc721Tokens` function: MANAGER reclaims ERC721 (NFT) tokens accidentally sent to the contract.

#### Events in CapabilityManagement
- `CapabilityRuleSetCreated`
- `CapabilityRuleSetUpdated`
- `CapabilityRuleSetApplied`
- `KeysAssociated`
- `ERC20TokenReclaimed`
- `ERC721TokenReclaimed`

### Staking contract

1. MANAGER: Special role. Use `AccessControl`  of contracts are implemented with. Manager role can be granted to multiple addresses.
2. `pause`: MANAGER temporarily pause any wxHOPR token deposit/withdrawal
3. `unpause`: MANAGER unpause wxHOPR token deposit/withdrawal
4. `reclaimErc20Tokens` function: MANAGER reclaims ERC20 fungible tokens accidentally sent to the contract.
5. `reclaimErc721Tokens` function: MANAGER reclaims ERC721 (NFT) tokens accidentally sent to the contract.
6. `daoMultisigAddress`: Stores the valid DAO multisig address. It can be updated by the MANAGER. Only `daoMultisigAddress` can pull/push assets from/to the Staking contract.
7. `adminKeyDisposableTokenAmount`: Store the disposable token amout of an address (admin key)
8. `chainKeyDisposableTokenAmount`: Maintain a record of the token disposable amount that can be accessed by all HoprChannels contracts for pulling wxHOPR tokens.
9. `addChainKeyDisposableTokenAmount`: Admin can move tokens from `adminKeyDisposableTokenAmount` to `chainKeyDisposableTokenAmount`
10. `subChainKeyDisposableTokenAmount`: Admin can move tokens from `chainKeyDisposableTokenAmount` to `adminKeyDisposableTokenAmount`
11. `withdrawlDisposableTokenAmount`: Admin withdraw tokens from the Staking contract
12. `daoDisposableTokenAmount`: Consider adding an extra functionality so the DAO can manage disposable liquidity. 
13. `addDaoDisposableTokenAmount`: Admin can move tokens from `adminKeyDisposableTokenAmount` to `daoDisposableTokenAmount`
14. `subDaoDisposableTokenAmount`: Admin can move tokens from `daoDisposableTokenAmount` to `adminKeyDisposableTokenAmount`
15. `tokensReceived` function: When receiving wxHOPR tokens, revert when the token is not wxHOPR token. When it's wxHOPR token, check
    - If the `operator` is a HoprChannels contract, if so, decode the `operatorData` payload and add/subtract balance from the respective `nodeDisposableTokenAmount`
    - If the `operator` field is the `daoMultisigAddress`, decode the `operatorData` payload and add/subtract balance from the respective `nodeDisposableTokenAmount`
    - If the `operator` field is not a registered HoprChannels contract address nor the `daoMultisigAddress` nor address-zero, ignore the `operatorData` field and only decode the `callData` field. Extract node's chain key and admin key for node's 2FA.

#### Events in staking contract
- `AdminKeyDisposableTokenAmountDeposited`
- `AdminKeyDisposableTokenAmountWithdrawn`
- `ChainKeyDisposableTokenAmountAdded`
- `ChainKeyDisposableTokenAmountSubtracted`
- `DaoDisposableTokenAmountAdded`
- `DaoDisposableTokenAmountSubtracted`
- `Paused`
- `Unpaused`
- `ERC20TokenReclaimed`
- `ERC721TokenReclaimed`

## :seedling: Demo

:::success
Compared with the current "HoprChannels" contract, a new field `bytes[] memory sigs` should be added to function ABIs, in order to verify signatures.
:::

1. Staking contract
```solidity!
mapping(address=>uint256) public adminKeyDisposableTokenAmount;
mapping(address=>uint256) public chainKeyDisposableTokenAmount;
// possible to add extra functionality so the DAO can manage disposable liquidity;
mapping(address=>uint256) public daoDisposableTokenAmount;

```
2. Signatures provided at start and its verification
```solidity
bytes32 DOMAIN_SEPARATOR = keccak256(
    abi.encode(
        keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'),
        keccak256(bytes('Capability')),
        keccak256(bytes(`monte_rosa_2_0`)), // network name
        block.chainid,
        address(this)
    )
);

struct Node {
    // HOPR node's local Ethereum address. See ## Design Rationale point 2
    address chainKeyAddress;
    // HOPR node's 2FA Ethereum address. See ## Design Rationale point 3
    address adminKeyAddress;
    // Consider adding peer id into this struct
    bytes peerId;
}

function onStart2FA(address chainKeyAddress, address adminKeyAddress, bytes calldata peerId, bytes[] calldata sigs) public {
    // check adminKeyAddress has added HOPR tokens to the staking contract.

    // build digest
    bytes32 onStartHash = keccak256(abi.encodePacked(
        '\x19\x01',
        DOMAIN_SEPARATOR(),
        keccak256(abi.encode(
            keccak256(encodeType(Node)), 
            address(chainKeyAddress), address(adminKeyAddress), bytes(peerId)
        ))
    ));
    
    // get required signers
    address[] memory requiredSigners = new address[](2);
    requiredSigners[0] = chainKeyAddress;
    requiredSigners[1] = adminKeyAddress;
    
    // verify signatures
    require(onSignatureValidation(msg.sender, onStartHash, sigs, requiredSigners), "ERROR");
    
    // store those state, emit events etc.
    
    // define `CAPABILITY_SET_CAPABILITY` for the chainKeyAddress to `ApprovalLevel.CHAIN_KEY`
}
```
3. capability checks
```solidity!
enum ApprovalLevel {
    CHAIN_KEY,
    ADMIN_KEY,
    EITHER,
    BOTH
}

function getRequiredSigners(address nodeAddr, bytes32 capability, bytes32 channelId) public view returns (address[] memory requiredSigners) {
    // get ApprovalLevel from `capabilityRuleSets` and `ruleSetApplied`

    // return addresses from `Node` according to the `ApprovalLevel`
}

// validate signatures and take the function msg.sender into consideration
// sigs has maximum two values
function onSignatureValidation(address caller, bytes32 digest, bytes[] memory sigs, address[] memory requiredSigners) public view returns (bool) {
    // verify signatures
    address signer0 = ECDSA.tryRecover(digest, sigs[0]);
    address signer1 = ECDSA.tryRecover(digest, sigs[1]);
    
    // check there's required signers
    for (uint256 i = 0; i < requiredSigners.length; j++) {
        if (
            requiredSigners[i] != signer0 &&
            requiredSigners[i] != signer1 &&
            requiredSigners[i] != caller
        ) {
            return false;
        }
    } 
    return true;
}
```
4. Define capabilities
```solidity!
struct CapabilityApproval {
    uint256 threshold;
    ApprovalLevel level;
}
// Capability hash
bytes32 public CAPABILITY_SET_CAPABILITY = keccak256("CAPABILITY_SET_CAPABILITY"); // the vel is set to `ApprovalLevel.ADMIN_KEY` by default at `onStart2FA()` function
bytes32 public CAPABILITY_ANNOUNCE = keccak256("CAPABILITY_ANNOUNCE");
bytes32 public CAPABILITY_OPEN_CHANNEL = keccak256("CAPABILITY_OPEN_CHANNEL");
// and more ...

// Capability rule set ID
//     => Capability hash 
//         => channel ID (if 0x0, then apply to all the channels) 
//             => Array<CapabilityApproval>
mapping(bytes32=>mapping(bytes32=>mapping(bytes32=>CapabilityApproval[]))) public capabilityRuleSets;
// Ethereum address of admin key
//     => HoprChannels address of a network
//         => Capability rule set ID
mapping(address=>(mapping=>bytes32)) public ruleSetApplied;

// Capability rule set ID
//     => Ethereum address of admin key
mapping(bytes32=>address) public ownerOfCapabilityRuleSets; 
// Ethereum address of admin key
//     => Capability rule set ID
mapping(address=>bytes32[]) public capabilityRuleSetsByWriter;

struct CapabilityPerChannel {
    address nodeAddr;
    bytes32 capabilityHash;
    bytes32 channelId;
    CapabilityApproval approval;
}

function setCapability(bytes32 capabilityHash, address nodeAddr, bytes32 channelId, CapabilityApproval approval, bytes[] memory sigs) public {
    // create digest for `setCapability` action,
    // build digest
    bytes32 setCapabilityHash = keccak256(abi.encodePacked(
        '\x19\x01',
        DOMAIN_SEPARATOR(),
        keccak256(abi.encode(
            keccak256(encodeType(CapabilityPerChannel)), 
            nodeAddr, capabilityHash, channelId, approval
        ))
    ));

    // get approval level (`ApprovalLevel.ADMIN_KEY`)
    // get required signer: Admin key address
    
    // validate the caller has the capability or has provided valid signature(s) to "set capability" 
    onSignatureValidation(msg.sender, setCapabilityHash, sigs, requiredSigners);
    
    // set capability, emit events...
}
```


## :books: Reference

[^oz_role]: For each role that you want to define, you’ll store a variable of type Role, which will hold the list of accounts with that role. Read more on [Role-based access control](https://docs.openzeppelin.com/contracts/2.x/access-control#role-based-access-control)