Skip to main content

Part 2: Staking using a threshold multisig (M-of-N) account

This tutorial walks you through setting up a multisig account to use to stake tez. It uses a multisig account with a threshold signature (M-of-N scheme), in which 2 of the 3 participants must approve of operations taken on behalf of the multisig account. These accounts can be controlled by multiple people sharing control of the account or by a single person who wants the additional security of spreading control of the account over multiple keys. For more information about staking, see Staking.

For the purposes of the tutorial, imagine that three users, Alice, Bob, and Charlie want to use a multisig account to control their staking position with a chosen baker. The tutorial follows these general steps that Alice, Bob, and Charlie must do:

  1. Creating the multisig account, distributing control over its key to the participant accounts, and destroying the original key
  2. Revealing and delegating the account
  3. Staking with the account

Each operation for the account (in this case including revealing, delegating, staking, and any transfers or smart contract calls) must be signed by 2 participants. For this reason, each operation involves these sub-steps:

  1. One participant generates the unsigned operation and shares it with the others.
  2. At least two of the participants sign the operation and share their signatures.
  3. One participant combines the participant signatures into a single signature.
  4. That participant signs the operation and submits it to the Tezos network.

For the purposes of the tutorial, the "participants" are different accounts on your local machine, stored in the Octez client's local wallet. In a real situation you can use accounts that are hosted on different systems and managed by different wallets.

If you have any trouble with the commands on this page, see the Troubleshooting section.

Prerequisites

Before you begin, make sure that you have version 23 or later of the Octez client and octez-codec program installed. See Installing the Octez client.

Setting up the multisig account

To create a multisig account, you create an account with the Octez client in a way similar to creating a regular account, but for the multisig account you split control of the account into multiple private keys based on the threshold. The threshold (2 of 3 keys in this case) cannot be changed after it is set. Then you distribute these private keys to the participants, who each import accounts based on them.

important

Whoever has the secret key of the multisig account can sign operations for it independently, getting around the authorization of the participants. For security, after you have imported the participant accounts, ensure that the secret key for the multisig account is destroyed. For this reason, the participants must trust the creator of the multisig account to destroy the private key.

  1. Create the unencrypted multisig account by running this command:

    octez-client gen keys multisig_staker -s bls

    This command is like the usual command to create an account except that it specifies the BLS signature scheme and therefore creates an account that starts with tz4.

    note

    The next two steps print unencrypted secret keys to the terminal. Beware of your surroundings and handle the keys with extra care to prevent them from being compromised.

  2. Print the address, public key, and private key of the new account:

    octez-client show address multisig_staker --show-secret

    Here is an example result:

    Hash: tz4SY87c9MTEknnhcmwfu3d7Qu8HqmgwfJkY
    Public Key: BLpk1tvA7VVVtccUz1VpCcvNgysintAReyaTdEzrCU6Ai9gftLMDQQNniKtmH5sMG7BqeR1uN4hy
    Secret Key: unencrypted:BLsk31ui8c4tQxfBEyY2FdbQdW8vmpoU5GUT4BhWPYijfjDqTQ3HVJ
  3. Split the key to implement the 2-of-3 signature scheme by running this command, where <PRIVATE_KEY> is the private key of the multisig account from the previous step:

    octez-client share bls secret key <PRIVATE_KEY> between 3 shares with threshold 2

    The output includes several fields, including the proof of possession (PoP) of the account and the three private keys for the participant accounts. Here is an example:

    { "public_key":
    "<MULTISIG_PK>",
    "public_key_hash": "<MULTISIG_ADDRESS>",
    "proof":
    "<MULTISIG_POP>",
    "secret_shares":
    [ { "id": 1,
    "secret_key":
    "<SECRET_KEY_1>" },
    { "id": 2,
    "secret_key":
    "<SECRET_KEY_2>" },
    { "id": 3,
    "secret_key":
    "<SECRET_KEY_3>" } ] }
    note

    These secret keys are tied to the IDs in the id fields in the output. When you submit operations, the signatures must be connected to those IDs, so keep track of the ID of each key.

    note

    The proof of possession in the response is a feature introduced in protocol proposal S that is related to an underlying change in the usage of BLS in the Tezos protocol necessary to implement both these features and aggregated attestations. For more information, start at tz4: BLS in the Octez and protocol documentation.

  4. Record the PoP of this multisig scheme (represented by the placeholder <MULTISIG_POP> above) because you will need it later.

  5. Import the secret keys for the first new account by running the octez-client import secret key command and passing the first secret key from the output of the previous step.

    For example, this command creates an account named alice with a private key represented by the placeholder <SECRET_KEY_1>:

    octez-client import secret key alice unencrypted:<SECRET_KEY_1>

    Of course, in a real scenario, you would distribute these keys in a secure manner to the other participants, but for the purposes of the tutorial you can use local accounts.

  6. Repeat the process to import the secret keys for the other participant accounts. In this tutorial, the examples use bob and charlie for the local aliases of these accounts.

  7. Make sure that you have written down the address of the multisig account (noted above as <MULTISIG_ADDRESS>) and its public key (noted above as <MULTISIG_PK>) and then run this command to delete the account and its private key:

    octez-client forget address multisig_staker -f
    important

    As described at the beginning of this section, it's important to delete the private key of the original account because if it still exists, it can be used to sign operations for the multisig account getting around the participant accounts.

  8. To keep track of the account's address and public key, add it back as a local alias by running this command, where <MULTISIG_PK> is the public key of the multisig account:

    octez-client import public key multisig_staker unencrypted:<MULTISIG_PK>

    Now you can get the address of the account with the command octez-client show address multisig_staker and use its alias locally, but you don't have its private key.

Now you have a single account controlled by three accounts, none of which can control it independently. In the next section, you use these participant accounts to send a transaction on behalf of the multisig account.

Revealing and delegating the multisig account

Multisig accounts must be revealed and delegated in order to stake, just like other accounts. However, the process is different, because (as explained at the top of this page) running any operation with the multisig account involves signing the operation with enough of the participants. In this section you sign an operation to reveal the account and delegate to a baker with whom the participants intend to stake.

  1. Set your installation of the Octez client to use a test network. For example, to use the Ghostnet testnet RPC endpoint at https://rpc.ghostnet.teztnets.com, run this command:

    octez-client -E https://rpc.ghostnet.teztnets.com config update

    For more information about test networks and other RPC endpoints, see Testing on testnets.

  2. From the faucet for the testnet, send 10,005 tez to the multisig account's address. The account must have some tez to be revealed and to pay for transaction fees on top of the 10,000 tez that you will stake in the next section.

  3. Choose a baker to delegate to and stake with. Make sure that the baker accepts external staking. For example, if you are using the Ghostnet test network, you can go to https://ghostnet.tzkt.io/bakers and find a baker that accepts staking there. The following steps refer to the address of the baker with the placeholder <BAKER_ADDRESS>.

  4. Get the counter of the multisig account by running this command, where <MULTISIG_ADDRESS> is the address of the multisig account:

    octez-client rpc get chains/main/blocks/head/context/contracts/<MULTISIG_ADDRESS>/counter
  5. Get the hash of a recent block to act as the branch to base the operation on by running this command:

    octez-client rpc get chains/main/blocks/head~2/hash

    The resulting hash is referred to in future steps by the placeholder <BRANCH>.

    note

    When you submit the signed operation, this branch must be a sufficiently recent block or the operation fails. The constant max_operations_time_to_live determines how recent the branch must be. For example, on Mainnet, this constant is 450 blocks, which gives you about one hour to get the operation signed by enough participants and submitted.

    To get the value of this constant on the network you are using, run this command, which requires the jq program:

    octez-client rpc get /chains/main/blocks/head/context/constants | jq | grep max_operations_time_to_live

    Then, multiply the value of this constant by the value of the minimal_block_delay constant to get the time duration in seconds.

  6. Generate the operation to reveal and delegate the account. You could do this in separate operations, but doing it in one step illustrates how you can batch operations:

    octez-codec encode 023-PtSeouLo.operation.unsigned from '{
    "branch": "<BRANCH>",
    "contents":
    [ { "kind": "reveal",
    "source": "<MULTISIG_ADDRESS>", "fee": "735",
    "counter": "<COUNTER_PLUS_1>", "gas_limit": "3251", "storage_limit": "0",
    "public_key": "<MULTISIG_PK>",
    "proof": "<MULTISIG_POP>" },
    { "kind": "delegation",
    "source": "<MULTISIG_ADDRESS>", "fee": "160",
    "counter": "<COUNTER_PLUS_2>", "gas_limit": "100", "storage_limit": "0",
    "delegate": "<BAKER_ADDRESS>" } ] }'

    This command uses values from previous steps, including:

    • The address and public key of the multisig account (<MULTISIG_ADDRESS> and <MULTISIG_PK>)
    • The PoP that was created when you split the account into participants (<MULTISIG_POP>)
    • The branch (<BRANCH>)
    • The address of the baker (<BAKER_ADDRESS>)
    • The next two counter values, which you can get by adding 1 and 2 to the current counter value from a previous step (<COUNTER_PLUS_1> and <COUNTER_PLUS_2>)

    It passes these values to the octez-codec program, which encodes them in binary based on the Seoul protocol. You can replace 023-PtSeouLo.operation.unsigned with the schema for the protocol that you want to use; see Encodings in the Octez and protocol documentation.

    The result is a string of bytes that represents the unsigned operation. These are the bytes that the participant accounts must sign to authorize the operation.

  7. Sign the operation as Alice by passing these bytes to the sign bytes command, which uses <UNSIGNED_BYTES> as a placeholder for the response from the previous command:

    octez-client sign bytes "0x03<UNSIGNED_BYTES>" for alice

    Note that this command prefixes the bytes with 0x03 because it is an encoded manager operation.

    The response is Alice's signature of the transaction.

  8. In the same way, sign the same bytes as Bob's account.

  9. Combine Alice and Bob's signatures to fully sign the operation by running this command, which uses the placeholders <ALICE_SIG> and <BOB_SIG> to represent their signatures:

    octez-client threshold BLS signatures '{
    "public_key": "<MULTISIG_PK>",
    "message": "03<UNSIGNED_BYTES>",
    "signature_shares": [
    { "id": 1, "signature": "<ALICE_SIG>" },
    { "id": 2, "signature": "<BOB_SIG>" } ]
    }'

    Note that the id fields in this command must match the accounts in the output of the share bls secret key command in the previous section. Also note that the unsigned operation bytes are prefixed with 03, not 0x03 as in a previous command.

    The response is the signature for the operation, which starts with BLsig.

  10. Sign the operation by running this command, which uses the same JSON parameter as the command to generate the operation with the addition of the signature field:

    octez-codec encode 023-PtSeouLo.operation from '{
    "branch": "<BRANCH>",
    "contents":
    [ { "kind": "reveal",
    "source": "<MULTISIG_ADDRESS>", "fee": "735",
    "counter": "<COUNTER_PLUS_1>", "gas_limit": "3251", "storage_limit": "0",
    "public_key": "<MULTISIG_PK>",
    "proof": "<MULTISIG_POP>" },
    { "kind": "delegation",
    "source": "<MULTISIG_ADDRESS>", "fee": "160",
    "counter": "<COUNTER_PLUS_2>", "gas_limit": "100", "storage_limit": "0",
    "delegate": "<BAKER_ADDRESS>" } ],
    "signature": "<COMBINED_SIGNATURE>" }'

    The value of the signature field, represented by the placeholder <COMBINED_SIGNATURE>, is the output of the previous command.

    The response is the signed operation as a series of bytes.

  11. Submit the operation by passing it to this command:

    octez-client rpc post /injection/operation with '"<SIGNED_OPERATION>"'

    Note that the signed operation is within both single and double quotes.

    The response is the hash of the submitted operation.

  12. Check the result of the operation by looking it up in the block explorer or passing its hash to the get receipt command:

    octez-client get receipt for <OPERATION_HASH>

The receipt notes that the operation was signed by the multisig account itself, not by the individual participants. It also includes both operations. Here is an example receipt:

$ octez-client get receipt for ongej6ymD9Fp6kw98eoRcRx3nc9uYEaAeDuhqQHnXL4KjC7mtXJ
Warning:

This is NOT the Tezos Mainnet.

Do NOT use your fundraiser keys on this network.

Operation found in block: BMBj6Pyv2PGXWq9cmYmiSpQxCBohuWA1vUyKN6asw32KHCJ1oNN (pass: 3, offset: 0)
Manager signed operations:
From: tz4SY87c9MTEknnhcmwfu3d7Qu8HqmgwfJkY
Fee to the baker: ꜩ0.000735
Expected counter: 6964
Gas limit: 3251
Storage limit: 0 bytes
Balance updates:
tz4SY87c9MTEknnhcmwfu3d7Qu8HqmgwfJkY ... -ꜩ0.000735
payload fees(the block proposer) ....... +ꜩ0.000735
Revelation of manager public key:
Contract: tz4SY87c9MTEknnhcmwfu3d7Qu8HqmgwfJkY
Key: BLpk1tvA7VVVtccUz1VpCcvNgysintAReyaTdEzrCU6Ai9gftLMDQQNniKtmH5sMG7BqeR1uN4hy
Proof of possession: BLsigANFeKUgWe2G7xxvu71q1PbBrC3DpugmcA47hb2qNNc3pXgWwG1wqwEKLycEHRy4uxzr3JxvhTmyBspaZy6fAiSFtxf8c31tejMryhJccxXHeJaHvnKhzueraTx4X2BSu61LVUysJW
This revelation was successfully applied
Consumed gas: 3250.815
Manager signed operations:
From: tz4SY87c9MTEknnhcmwfu3d7Qu8HqmgwfJkY
Fee to the baker: ꜩ0.00016
Expected counter: 6965
Gas limit: 100
Storage limit: 0 bytes
Balance updates:
tz4SY87c9MTEknnhcmwfu3d7Qu8HqmgwfJkY ... -ꜩ0.00016
payload fees(the block proposer) ....... +ꜩ0.00016
Delegation:
Contract: tz4SY87c9MTEknnhcmwfu3d7Qu8HqmgwfJkY
To: tz1TGKSrZrBpND3PELJ43nVdyadoeiM1WMzb
This delegation was successfully applied
Consumed gas: 100

Staking with the multisig account

Now that you have the multisig account revealed and delegated, you can stake with it just like a regular user account. Like the previous operations, you must build the operation and get enough participants to sign it.

  1. Get the new counter of the multisig account by running this command, where <MULTISIG_ADDRESS> is the address of the multisig account:

    octez-client rpc get chains/main/blocks/head/context/contracts/<MULTISIG_ADDRESS>/counter
  2. Get the hash of a recent block to act as the branch to base the operation on by running this command:

    octez-client rpc get chains/main/blocks/head~2/hash

    The resulting hash is referred to in future steps by the placeholder <BRANCH>.

  3. Like you did in the previous section, generate the operation to stake with the account:

    octez-codec encode 023-PtSeouLo.operation.unsigned from '{
    "branch": "<BRANCH>",
    "contents":
    [ { "kind": "transaction",
    "source": "<MULTISIG_ADDRESS>", "fee": "808",
    "counter": "<COUNTER_PLUS_1>", "gas_limit": "5134", "storage_limit": "0",
    "amount": "<AMOUNT_TO_STAKE>",
    "destination": "<MULTISIG_ADDRESS>",
    "parameters":
    { "entrypoint": "stake", "value": { "prim": "Unit" } } } ] }'

    This command includes the counter value plus one and the amount to stake in mutez, in this case 10000000000 for 10,000 tez. The stake operation is implemented internally as a transfer from the account to itself via the stake entrypoint.

    The result is a string of bytes that the participants must sign.

  4. Sign the operation as Alice by passing these bytes to the sign bytes command, which uses <UNSIGNED_BYTES> as a placeholder for the response from the previous command:

    octez-client sign bytes "0x03<UNSIGNED_BYTES>" for alice

    Note that this command prefixes the bytes with 0x03 because it is a (batched) manager operation.

    The response is Alice's signature of the transaction.

  5. In the same way, sign the same bytes as Charlie's account.

  6. Combine Alice and Charlie's signatures to fully sign the operation by running this command, which uses the placeholders <ALICE_SIG> and <CHARLIE_SIG> to represent their signatures:

    octez-client threshold BLS signatures '{
    "public_key": "<MULTISIG_PK>",
    "message": "03<UNSIGNED_BYTES>",
    "signature_shares": [
    { "id": 1, "signature": "<ALICE_SIG>" },
    { "id": 3, "signature": "<CHARLIE_SIG>" } ]
    }'

    Note that the id fields in this command must match the accounts in the output of the share bls secret key command in the previous section. Also note that the unsigned operation bytes are prefixed with 03, not 0x03 as in a previous command.

    The response is the signature for the operation, which starts with BLsig.

  7. Sign the operation by running this command, which uses the same JSON parameter as the command to generate the operation with the addition of the signature field:

    octez-codec encode 023-PtSeouLo.operation from '{
    "branch": "<BRANCH>",
    "contents":
    [ { "kind": "transaction",
    "source": "<MULTISIG_ADDRESS>", "fee": "808",
    "counter": "<COUNTER_PLUS_1>", "gas_limit": "5134", "storage_limit": "0",
    "amount": "<AMOUNT_TO_STAKE>",
    "destination": "<MULTISIG_ADDRESS>",
    "parameters":
    { "entrypoint": "stake", "value": { "prim": "Unit" } } } ],
    "signature": "<COMBINED_SIGNATURE>" }'

    The content of this JSON parameter must match the operation you created in a previous step, except for the signature field.

    The response is the signed operation as a series of bytes.

  8. Submit the operation by passing it to this command:

    octez-client rpc post /injection/operation with '"<SIGNED_OPERATION>"'

    Note that the signed operation is within both single and double quotes.

    The response is the hash of the submitted operation.

  9. Check the result of the operation by looking it up in the block explorer or passing its hash to the get receipt command:

    octez-client get receipt for <OPERATION_HASH>

Now the tez is staked and the account starts receiving rewards automatically. When it's time to unstake the tez, the participant accounts must create an unstaking operation and sign it as they signed the other operations. Similarly, they must co-sign any transfers or smart contract calls from this account.

Troubleshooting

Errors with the octez-client sign bytes command:

  • If this command throws the error Invalid bytes, expecting hexadecimal notation, ensure that the bytes to be signed are prefixed with the code 0x03, as required in the Micheline encoding.

Errors with the octez-client threshold bls signatures command:

  • If this command throws an error that says that it failed to produce the signature, make sure that the message field is prefixed with only 03 in the JSON encoding, not 0x03.

  • Make sure you signed with the correct participant accounts and enough participant accounts.

  • Make sure that the participant accounts are matched with the correct IDs.

Errors with the command octez-client rpc post /injection/operation with '"<SIGNED_OPERATION>"':

  • If this command throws an error that says that the operation failed because it is branched on a block that is too old, the branch that you based the operation on is out of date. You may need to complete these steps more quickly or automate them to complete them in time. You can calculate the allowable time by checking the time to live, as described in Revealing and delegating the multisig account.

  • If the error says that the operation signature is invalid, make sure that the contents of the operations that you passed to the octez-codec commands match.

  • If the error says that the counter is already used, make sure that you have added one to the current counter value.

The octez-client rpc post /injection/operation command returns an operation hash but this hash does not exist on the block explorer and the octez-client get receipt for command fails:

  • Check that the source account has enough tez to pay the fees.

  • The fee field for the operation may be too low.

Summary

In this tutorial you split a single tz4 account into multiple participant accounts to create a multi-signature account. No one knows the private key for the account, so no one person can control it. Instead, each subsequent operation must be signed by a certain number of participants and aggregated by one participant.

From here, you can use the account much as an ordinary user account, but with the enhanced security of requiring multiple signatures to create operations.