Multi-Signature

Multiple signature functions enable permission grading, where each permission can be associated with multiple private keys. This facilitates the implementation of multi-person joint control of accounts. This guide provides a step-by-step walkthrough of Pollux's multi-signature implementation and design.

Design

The scheme encompasses three privilege levels: owner, duration, and active privileges. The owner privilege empowers the execution of all contracts, the duration privilege is designated for super delegates, and the active privilege is a customizable privilege that can be combined with permission sets.

Structure Description

  1. Account Modification

message Account { 
   ... 
   Permission owner_permission = 31;
   Permission witness_permission = 32;
   repeated Permission active_permission = 33;
}

Three permission attributes have been incorporated into the account structure: owner_permission, witness_permission, and active_permission. The active_permission is represented as a list, allowing for specification of up to 8 permissions.

2. Contract Type Modification

message Transaction {
    message Contract {
        enum ContractType {
            AccountCreateContract = 0; 
            // ... (other contract types) ...
            AccountPermissionUpdateContract = 46;
        }
    }
}

Added a transaction type AccountPermissionUpdateContract to update account permissions.

3. AccountPermissionUpdateContract

message Transaction {
    message AccountPermissionUpdateContract {
        bytes owner_address = 1;
        Permission owner = 2;
        Permission witness = 3;
        repeated Permission actives = 4;
    }
}

Here's the information presented in tabular form:

ParameterDescription

owner_address

The address of the account to be modified

owner

Modified owner permission

witness

Modified witness permission (if it is a witness)

actives

Modified actives permission

This interface supersedes the existing account permissions. Therefore, even if you only intend to modify the owner permissions, both the witness (in the case of a witness account) and actives permissions must also be configured.

4. Permission

message Permission {
    enum PermissionType {
        Owner = 0;
        Witness = 1;
        Active = 2;
    }
    PermissionType type = 1;
    int32 id = 2;
    string permission_name = 3;
    int64 threshold = 4;
    int32 parent_id = 5;
    bytes operations = 6;
    repeated Key keys = 7;
}

Here is the information presented in a tabular format, with the requested replacements:

ParameterDescription

PermissionType

Permission type, currently only supports three permissions.

id

The value is automatically set by the system, with Owner id=0 and Witness id=1. Active id is incremented from 2 onwards. When the contract is executed, the id is used to specify which permission to use. For example, if the owner permission is used, the id is set to 0.

permission_name

Permission name, set by the user, limited to 32 bytes in length.

threshold

Threshold, the corresponding operation is allowed only when the sum of the weights of the participating signatures exceeds the domain value. Requires a maximum value less than the Long type.

parent_id

Currently only 0

operations

A total of 32 bytes (256 bits), each representing the authority of a contract, a 1 means the authority to own the contract. Please refer to below detailed example: "Example of operations in active permissions"

keys

The address and weight that jointly own the permission can be up to 5 keys.

Please refer to the detailed example below for "Example of operations in active permissions."

5. Key

message Key {
   bytes address = 1;
   int64 weight = 2;
 }

Here is the information presented in a tabular format:

ParameterDescription

address

Address with this privilege

weight

This address has weight for this permission

6.Transaction Modification

message Transaction {
      ...
     int32 Permission_id = 5;
 }

Add a Permission_id field to the transaction, corresponding to Permission.id, which specifies which permission to use. The default is 0, which is the owner permission. It is not allowed to be 1, because the witness permission is only used for block creation and is not used to sign the transaction.

Owner Permission

The Owner Permission holds the highest privilege within the account, facilitating control over user ownership, adjustment of the privilege structure, and the execution of all contracts.

Key characteristics of the Owner privilege include:

  • Modification of the OwnerPermission address by OwnerPermission.

  • When OwnerPermission is empty, the account address is assumed to possess owner permission by default.

  • Upon the creation of a new account, the account address is automatically included in the OwnerPermission, with a default domain value of 1. The keys include the only address, and the weight is set to 1.

  • When the permissionId is not specified during contract execution, OwnerPermission is utilized by default.

Witness Permission

The super representative can use this privilege to manage the block nodes. Non-witness accounts do not have this permission.

Example of usage scenario: A super representative deploys a block program on the cloud server. For account security, you can assign the block permission to another address. Since the address only has the outbound permission, there is no POX rollout permission, and even if the private key on the server is compromised, POX will not be lost.

Witness block production node configuration:

No special configuration is required when the witness permissions are not modified. The block node modified to witness permission needs to be reconfigured. The configuration items are as follows:

#config.conf

// Optional.The default is empty.
// It is used when the witness account has set the witnessPermission.
// When it is not empty, the localWitnessAccountAddress represents the address of the witness account,
// and the localwitness is configured with the private key of the witnessPermissionAddress in the witness account.
// When it is empty,the localwitness is configured with the private key of the witness account.
// Optional, default is empty.
// Used to set the durationPermission when the witness account is set.
// When the value is not empty, localWitnessAccountAddress represents the address of the witness account, and localwitness is the private key of the address in the durationPermission.
// When the value is empty, localwitness is configured as the private key of the witness account.

//localWitnessAccountAddress =

localwitness = [
  f4df789d3210ac881cb900464dd30409453044d2777060a0c391cbdf4c6a4f57
]

Active Permissions

Active permissions are designed to offer a versatile blend of permissions, allowing configurations such as granting the authority to execute specific functions like creating accounts and transferring funds.

Key features of Active permissions include:

  • Modification by the address with OwnerPermission.

  • Modification by the address authorized to execute AccountPermissionUpdateContract.

  • Support for up to 8 combinations.

  • Automatic increment of permission id starting from 2.

  • Automatic creation of an Active permission when an account is newly established, with the account's address filled in. The default threshold value is 1, including only the account address in the keys with a weight of 1.

Cost

When employing the account update permission, specifically the AccountPermissionUpdate contract, there is a charge of 100POX. For multi-signature transactions, which involve two or more signatures in the transaction, an additional fee of 1POX is incurred, in addition to the transaction fee. The proposed adjustments can modify the mentioned fees.

API

Modify Permissions

The steps to modify permissions using the AccountPermissionUpdateContract are as follows:

  1. Utilize the interface getaccount to query the account and retrieve the original permissions.

  2. Modify the permissions as required.

  3. Create a contract and obtain the necessary signatures.

  4. Send a transaction with AccountPermissionUpdateContract to update the permissions.

HTTP Demo

http://{{host}}:{{port}}/wallet/accountpermissionupdate

{
  "owner_address": "41ffa9466d5bf6bb6b7e4ab6ef2b1cb9f1f41f9700",
  "owner": {
    "type": 0,
    "permission_name": "owner",
    "threshold": 2,
    "keys": [
      {
        "address": "41F08012B4881C320EB40B80F1228731898824E09D",
        "weight": 1
      },
      {
        "address": "41DF309FEF25B311E7895562BD9E11AAB2A58816D2",
        "weight": 1
      },
      {
        "address": "41BB7322198D273E39B940A5A4C955CB7199A0CDEE",
        "weight": 1
      }
    ]
  },
  "actives": [
    {
      "type": 2,
      "permission_name": "active0",
      "threshold": 3,
      "operations": "7fff1fc0037e0000000000000000000000000000000000000000000000000000",
      "keys": [
        {
          "address": "41F08012B4881C320EB40B80F1228731898824E09D",
          "weight": 1
        },
        {
          "address": "41DF309FEF

Example of operations in active permissions

"operations" is a hexadecimal-coded sequence in little-endian byte order, comprising 32 bytes (256 bits). Each bit within this sequence represents the authority of a system contract type. The nth bit signifies the authority of the system contract type with ID n. A value of 1 indicates the authority to execute the specific system contract type, while a value of 0 signifies the absence of authority. Please refer to the table below for the ID values associated with different system contract types:

System Contract TypeID Value

Pollux

0

PVM

1

PRC10

2

POX

3

Here is the information presented in a tabular format with the requested replacements:

System Contract TypeIDDescription

AccountCreateContract

0

Create Account

TransferContract

1

POX transfer

TransferAssetContract

2

PRC10 token transfer

VoteAssetContract

3

Unused

VoteWitnessContract

4

Vote for Super Representatives

WitnessCreateContract

5

Apply to be a Super Representative Candidate

AssetIssueContract

6

Issue PRC10 Tokens

WitnessUpdateContract

8

Update website URLs for Super Representative candidates

ParticipateAssetIssueContract

9

Buy PRC10 Tokens

AccountUpdateContract

10

Update account name

FreezeBalanceContract

11

Stake1.0 stake

UnfreezeBalanceContract

12

Unstake POX staked in Stake1.0 phase

WithdrawBalanceContract

13

Withdraw rewards

UnfreezeAssetContract

14

Unfreeze issued PRC10 tokens

UpdateAssetContract

15

Update PRC10 token parameters

ProposalCreateContract

16

Create proposal

ProposalApproveContract

17

Approve proposal

ProposalDeleteContract

18

Delete proposal

SetAccountIdContract

19

Set account ID

CreateSmartContract

30

Create a smart contract

TriggerSmartContract

31

Trigger smart contract

UpdateSettingContract

33

Update consume_user_resource_percent

ExchangeCreateContract

41

Create an exchange

ExchangeInjectContract

42

Exchange Inject

ExchangeWithdrawContract

43

Exchange Withdraw

ExchangeTransactionContract

44

Bancor Transaction

UpdateEnergyLimitContract

45

Adjust the energy limit provided by the smart contract deployer

AccountPermissionUpdateContract

46

Update account permissions

ClearABIContract

48

Clear contract ABI

UpdateBrokerageContract

49

Update SR Brokerage

ShieldedTransferContract

51

Shielded transactions

FreezeBalanceV2Contract

54

Stake POX

UnfreezeBalanceV2Contract

55

Unstake POX

WithdrawExpireUnfreezeContract

56

Withdraw the unstaked principal that has passed the lock-up period

DelegateResourceContract

57

Resource delegate

UnDelegateResourceContract

58

Cancel resource delegate

CancelAllUnfreezeV2Contract

59

Cancel all unstakes

To enhance user readability, let's use binary big-endian byte order as an example to explain how to calculate the value of operations. The numbering of digits starts from 0, corresponding to the ID of the system contract type from left to right. Convert a binary big-endian byte sequence to a hexadecimal little-endian byte sequence; this will be the value of operations. Please refer to the examples below:

Operations AllowedBinary Code (big-endian)Binary Code (little-endian)Hex Code (little-endian)

TransferContract(1) & VoteWitnessContract(4)

01001000 00000000 00000000 ...

00010010 00000000 00000000 ...

12 00 00 ...

TransferContract(1) & UpdateAssetContract(15)

01000000 00000001 00000000 ...

000000010 10000000 00000000 ...

02 80 00 ...

All system contracts

11111110 11111111 11111000 ...

01111111 11111111 00011111 ...

7F FF 1F ...

Example of calculation of operations in active authority

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.bouncycastle.util.encoders.Hex;

enum ContractType {
    UndefinedType(-1),
    AccountCreateContract(0),
    TransferContract(1),
    TransferAssetContract(2),
    VoteAssetContract(3),
    VoteWitnessContract(4),
    WitnessCreateContract(5),
    AssetIssueContract(6),
    WitnessUpdateContract(8),
    ParticipateAssetIssueContract(9),
    AccountUpdateContract(10),
    FreezeBalanceContract(11),
    UnfreezeBalanceContract(12),
    WithdrawBalanceContract(13),
    UnfreezeAssetContract(14),
    UpdateAssetContract(15),
    ProposalCreateContract(16),
    ProposalApproveContract(17),
    ProposalDeleteContract(18),
    SetAccountIdContract(19),
    CustomContract(20),
    CreateSmartContract(30),
    TriggerSmartContract(31),
    GetContract(32),
    UpdateSettingContract(33),
    ExchangeCreateContract(41),
    ExchangeInjectContract(42),
    ExchangeWithdrawContract(43),
    ExchangeTransactionContract(44),
    UpdateEnergyLimitContract(45),
    AccountPermissionUpdateContract(46),
    ClearABIContract(48),
    UpdateBrokerageContract(49),
    ShieldedTransferContract(51),
    MarketSellAssetContract(52),
    MarketCancelOrderContract(53),
    FreezeBalanceV2Contract(54),
    UnfreezeBalanceV2Contract(55),
    WithdrawExpireUnfreezeContract(56),
    DelegateResourceContract(57),
    UnDelegateResourceContract(58),
    CancelAllUnfreezeV2Contract(59);

    private int num;

    ContractType(int num) {
        this.num = num;
    }

    public static ContractType getContractTypeByNum(int num) {
        for (ContractType type : ContractType.values()) {
            if (type.getNum() == num)
                return type;
        }
        return ContractType.UndefinedType;
    }

    public int getNum() {
        return num;
    }
}

public class OperationsEncoderAndDecoder {

    // Description: get operations code according to the input contract types
    public static String operationsEncoder(ContractType[] contractId) {

        List<ContractType> list = new ArrayList<>(Arrays.asList(contractId));
        byte[] operations = new byte[32];
        list.forEach(e -> {
            int num = e.getNum();
            operations[num / 8] |= (1 << num % 8);
        });

        return Hex.toHexString(operations);
    }

    // Description: get all allowable contract types according to the operations code
    public static List<String> operationsDecoder(String operations) {

        List<String> contractIDs = new ArrayList<>();
        byte[] opArray = Hex.decode(operations);
        for (int i = 0; i < 32; i++) // 32 bytes
        {
            for (int j = 0; j < 8; j++) {
                if ((opArray[i] >> j & 0x1) == 1) {
                    contractIDs.add(ContractType.getContractTypeByNum(i * 8 + j).name());
                }
            }
        }
        return contractIDs;
    }

    public static void main(String[] args) {
        ContractType[] contractID = { ContractType.TransferContract, ContractType.VoteWitnessContract,
                ContractType.FreezeBalanceV2Contract };
        String operations = operationsEncoder(contractID);
        System.out.println(operations);
        // output: 1200000000004000000000000000000000000000000000000000000000000000

        List<String> contractIDs = operationsDecoder(operations);
        contractIDs.forEach(e -> {
            System.out.print(e + " ");
        });
        // output: TransferContract VoteWitnessContract FreezeBalanceV2Contract
    }
}

Construct and execute multi-signature transactions

Create a transaction following the same construction process as a non-multiple signature transaction:

  1. Specify the Permission_id, with the default being 0, indicating the owner-permission.

  2. User A signs the post-signature transaction to B through other means.

  3. User B signs, and the signed transaction is sent to C by other means.

  4. The last user who completed the signature broadcasts the transaction to the node. Verify that the sum of the weights of the multi-signature is greater than the domain value during N+1, accept the transaction; otherwise, reject the transaction.

Query API related to multi-sign transaction:

  1. Query Signed Address

curl -X POST  http://127.0.0.1:8090/wallet/getapprovedlist -d '{"transaction"}'
 
rpc GetTransactionApprovedList(Transaction) returns (TransactionApprovedList) { }
  1. Query Transaction Signature Weight

curl -X POST  http://127.0.0.1:8090/wallet/getsignweight -d '{"transaction"}'
 
rpc GetTransactionSignWeight (Transaction) returns (TransactionSignWeight) {}

Upon the creation of an account in Pollux, the owner-permission and an active-permission are automatically generated. The owner-permission consists of one key, where both permissions and thresholds are set to 1. Similarly, the active-permission also includes a key with permissions and thresholds set at 1. The operations are represented as "7fff1fc0033efb07000000000000000000000000000000000000000000000000," indicating support for all operations except AccountPermissionUpdateContract.

Last updated