πŸ‘¨β€πŸ’»HID Node

Hypersign Identity Network (HID Node) is built on top of Cosmos SDK. This documentation only covers the SSI module and the basic commands of Cosmos SDK modules. It is recommended to have a good understanding of Cosmos SDK. Please refer Cosmos SDK docs here.

The knowledge of Protocol Buffers is also essential.

Project Structure

Following is the high level folder structure of HID Node

β”œβ”€β”€ app
β”œβ”€β”€ cmd
β”œβ”€β”€ docs
β”œβ”€β”€ proto
β”‚   └── ssi
β”‚       └── v1
β”‚           β”œβ”€β”€ credential.proto
β”‚           β”œβ”€β”€ did.proto
β”‚           β”œβ”€β”€ genesis.proto
β”‚           β”œβ”€β”€ query.proto
β”‚           β”œβ”€β”€ schema.proto
β”‚           └── tx.proto
β”œβ”€β”€ scripts
β”œβ”€β”€ tests
β”œβ”€β”€ testutil
β”œβ”€β”€ third_party
β”‚   └── proto
β”‚       β”œβ”€β”€ gogoproto
β”‚       β”‚   └── gogo.proto
β”‚       └── google
β”‚           └── protobuf
β”‚               └── descriptor.proto
└── x
    └── ssi
        β”œβ”€β”€ abci.go
        β”œβ”€β”€ client
        β”‚   └── cli
        β”‚       β”œβ”€β”€ query.go
        β”‚       β”œβ”€β”€ query_ssi.go
        β”‚       β”œβ”€β”€ tx.go
        β”‚       β”œβ”€β”€ tx_ssi.go
        β”‚       └── tx_utils.go
        β”œβ”€β”€ genesis.go
        β”œβ”€β”€ genesis_test.go
        β”œβ”€β”€ handler.go
        β”œβ”€β”€ keeper
        β”‚   β”œβ”€β”€ credential.go
        β”‚   β”œβ”€β”€ did.go
        β”‚   β”œβ”€β”€ document_verification
        β”‚   β”‚   β”œβ”€β”€ common_checks.go
        β”‚   β”‚   β”œβ”€β”€ credential_verification.go
        β”‚   β”‚   β”œβ”€β”€ did_verification.go
        β”‚   β”‚   └── vars.go
        β”‚   β”œβ”€β”€ grpc_query_credential.go
        β”‚   β”œβ”€β”€ grpc_query_did.go
        β”‚   β”œβ”€β”€ grpc_query.go
        β”‚   β”œβ”€β”€ grpc_query_schema.go
        β”‚   β”œβ”€β”€ keeper.go
        β”‚   β”œβ”€β”€ msg_server_credential.go
        β”‚   β”œβ”€β”€ msg_server_did.go
        β”‚   β”œβ”€β”€ msg_server.go
        β”‚   β”œβ”€β”€ msg_server_schema.go
        β”‚   β”œβ”€β”€ schema.go
        β”‚   └── signature_verification.go
        β”œβ”€β”€ module.go
        β”œβ”€β”€ tests
        β”œβ”€β”€ types
        └── utils

Protobuf

The interaction between a client and HID Node happens through RPC, built using Protobuf. The proto files are defined under proto/ssi/v1. The are two RPC service namely Msg and Query, defined in proto/ssi/v1/tx.proto and proto/ssi/v1/query.proto respectively. List of HID Node RPCs are:

Transaction Based

  • MsgCreateDID

  • MsgUpdateDID

  • MsgDeactivateDID

  • MsgCreateSchema

  • MsgRegisterCredentialStatus

Query Based

  • QueryDidDocument

  • QueryDidDocuments

  • QuerySchema

  • QuerySchemas

  • QueryCredential

  • QueryCredentials

The generation of Golang code is done by the script scripts/protogenc.sh. The generated files are present in x/ssi/types.

DID Document

The DID-Core W3C Specification has been followed to frame the structure of a DID Document. It's proto representation is mentioned in proto/ssi/v1/did.proto. The message DidDocumentState is stored on chain.

// proto/ssi/v1/did.proto

message DidDocumentState {
  Did didDocument = 1;
  Metadata didDocumentMetadata = 2;
}

Schema Document

The proto representation is mentioned in proto/ssi/v1/schema.proto. The message Schema is stored on chain.

// proto/ssi/v1/schema.proto

message Schema {
    string type = 1;
    string modelVersion = 2;
    string id = 3;
    string name = 4;
    string author = 5;
    string authored = 6;
    SchemaProperty schema = 7;
    SchemaProof proof = 8;
}

Credential Status Document

The proto representation is mentioned in proto/ssi/v1/credential.proto. The message Credential is stored on chain.

// proto/ssi/v1/credential.proto

message Credential {
    Claim claim = 1;
    string issuer = 2;
    string issuanceDate = 3;
    string expirationDate = 4;
    string credentialHash = 5;
    CredentialProof proof = 6;
}

x/ssi Module

In Cosmos SDK, every operations related to blockchain such as staking, delegation, token trasfer, etc are handled by different modules. They are defined in the x directory of Cosmos SDK (See here). For instance, x/bank module handles the functionality of token transfer. x/ssi module lets you store documents such as Decentralised Identifiers (DID), Schema Document and Verifiable credential Status on chain.

Quick Overview

x/ssi/client/cli: CLI Client for sending SSI-based transactions to and querying data from Blockchain.

x/ssi/keeper: Interaction with the state of blockchain

x/ssi/genesis.go: - Initialising and Exporting of genesis variables of x/ssi module, such as chain_namespace. - For public mainnet chain, chain_namespace should be empty. In case of testnet, the name of the testnet should be assigned. If you are running a private network, you can specify your own chain namepsace.

x/ssi/modules.go: Defines the interface for ssi module

Keeper

Keepers provides an abstraction to interact with the state of the blockchain. The store is a data structure which persists the state. The Get and Set methods of the store are handled by the Keeper. There are Keeper functions defined for each of the RPCs. Transaction-based RPCs share a similar workflow, while Query-based share different workflow similarity among themeselves.

Transaction Based Keepers

Let's take the example of MsgCreateDID

The proto definition of RPC is as follows:

// proto/ssi/v1/tx.proto

service Msg {
    // ----
    rpc CreateDID(MsgCreateDID) returns (MsgCreateDIDResponse);
    // ---
}

message MsgCreateDID {
  Did didDocString = 1; // from proto/ssi/v1/did.proto
  repeated SignInfo signatures = 2; // from proto/ssi/v1/did.proto
  string creator = 3; // blockchain account address who will be signing the transaction
}

message MsgCreateDIDResponse {
  uint64 id = 1;
}

Pseudocode:

// x/ssi/keeper/msg_server_did.go

func (k msgServer) CreateDID(goCtx context.Context, msg *types.MsgCreateDID) (*types.MsgCreateDIDResponse, error) {
    ctx <- unwrap SDK Context from `goCtx`

    // Get the document from `msg`
    didDocument <- get the DID Document from `msg` arg
    signature <- get the signature information from `msg` arg

    // Verfication of Document //
    err = ValidateDidDocumentElements(didDocument)
    if err != nil {
        return nil, err
    }

    err = CheckIfDidDocumentExistsOnChain(didDocument.Id)
    if err != nil {
        return nil, err
    }

    err = CheckValidityOfDidControllers(didDocument)
    if err != nil {
        return nil, err
    }

    err = CheckIfSignaturesAreValid(didDocument, signature)
    if err != nil {
        return nil, err
    }
    // ..................... //

    // Storing DID Document on Store //
    var metadata *types.Metadata = CreateMetadataForDIDDocument()

    // Forming The DID Document State to be stored on chain
    didDocumentState := types.DidDocumentState{
		DidDocument:         didDocument,
		DidDocumentMetadata: &metadata,
	}

    CreateDidDocumentInStore(ctx, didDocumentState)
    // ---------------------------- //
}

Query Based Keepers

Let's take the example of Query RPC QueryDidDocument

The proto definition of RPC is as follows:

service Query {
    // ---
    rpc QueryDidDocument(QueryDidDocumentRequest) returns (QueryDidDocumentResponse) {
        option (google.api.http).get = "/hypersign-protocol/hidnode/ssi/did/{didId}";
    }
    // ---
}

message QueryDidDocumentRequest {
  string didId = 1;
}

message QueryDidDocumentResponse {
  Did didDocument = 1;
  Metadata didDocumentMetadata = 2;
}

Pseudocode:

// x/ssi/keeper/grpc_query_did.go

func (k Keeper) QueryDidDocument(goCtx context.Context, req *types.QueryDidDocumentRequest) (*types.QueryDidDocumentResponse, error) {
    ctx <- unwrap SDK Context from `goCtx`
    didId <- get did Id from `req`

    didDocumentState, err := GetDidDocumentFrom(didId)
    if err != nil {
        return nil, err
    }

    return &types.QueryDidDocumentResponse{
		DidDocument:         didDoc.GetDidDocument(),
		DidDocumentMetadata: didDoc.GetDidDocumentMetadata(),
	}, nil
}

Store

The Store is a Key-Value structure responsible for persisting the state of chain. The store can have multiple subspaces, which acts as individual KV Stores. The identification of these subspaces, specifically meant for x/ssi module, are mentioned in x/ssi/types/keys.go.

Substore Namespace

The following table describes the substores:

NamespaceKeyValue

DidKey

DID Document Id

DID Document

DidCountKey

[]byte(DidCountKey)

Registered DID Documents Count

SchemaKey

Schema Document ID

Schema Document

SchemaCountKey

[]byte(SchemaCountKey)

Registered Schema Documents Count

CredKey

Credential ID

CredentialStatus Document

CredCountKey

[]byte(CredCountKey)

Registered CredentialStatus Documents Count

DidNamespaceKey

[]byte(DidNamespaceKey)

Chain Namespace

Store Functions

The store functions comprise of operations such as addition to, change from and query by key from the Store. These are defined in the following files:

x
└── ssi
    └── keeper
       β”œβ”€β”€ credential.go
       β”œβ”€β”€ did.go
       └── schema.go

Consider the Get and Set methods related to DID Document:

Get:

// x/ssi/keeper/did.go

// Adding a aew DID Document to store
func (k Keeper) CreateDidDocumentInStore(ctx sdk.Context, didDoc *types.DidDocumentState) uint64 {
    // Get the registered Count
    count := k.GetDidCount(ctx)

    // Initialises the store of subspace DidKey
    store := prefix.NewStore(ctx.KVStore(k.storeKey), []byte(types.DidKey))

    // In DidKey store:
    // Key: DID Document Id
    // Value: DID Document
    id := didDoc.GetDidDocument().GetId()
    didDocBytes := k.cdc.MustMarshal(didDoc)

    // The KV pair is Set in Store in byte array form
    store.Set([]byte(id), didDocBytes)
    
    // Increment registered DID Document count by 1
    k.SetDidCount(ctx, count+1)
    return count
}

Set:

// x/ssi/keeper/did.go

// Get the DID Document from Store
func (k Keeper) GetDid(ctx *sdk.Context, id string) (*types.DidDocumentState, error) {
    // Initialises the store of subspace DidKey
    store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.DidKey))

    var didDocState types.DidDocumentState
    // Fetch the DID Document from store (in byteArray form)
    var bytes = store.Get([]byte(id))
    // Unmarshal byteArray into DidDocumentState type
    if err := k.cdc.Unmarshal(bytes, &didDocState); err != nil {
        return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidType, err.Error())
    }

    return &didDocState, nil
}

Begin-Block

The status of every Registered Verifiable Credential (VC) is check at the beginning of each block. If the current block datetime is greater than the expiration datetime of a VC, the Status is set to Expired.

The BeginBlocker function is written in x/ssi/abci.go

// x/ssi/abci.go

// BeginBlocker is called at the beginning of every block
func BeginBlocker(ctx sdk.Context, k keeper.Keeper) {
	// Set all the credential status that have passed their expiration date
	// to Expired
	if err := k.SetCredentialStatusToExpired(ctx); err != nil {
		panic(err)
	}
}


// x/ssi/keeper/credential.go

func (k Keeper) SetCredentialStatusToExpired(ctx sdk.Context) error {
	store := prefix.NewStore(ctx.KVStore(k.storeKey), []byte(types.CredKey))
	iterator := sdk.KVStorePrefixIterator(store, []byte{})
	defer iterator.Close()

	for ; iterator.Valid(); iterator.Next() {
		var cred types.Credential
		if err := k.cdc.Unmarshal(iterator.Value(), &cred); err != nil {
			return err
		}

		currentBlockTime := ctx.BlockTime()
		expirationDate, err := time.Parse(time.RFC3339, cred.GetExpirationDate())
		if err != nil {
			return err
		}

		// Set the Credential Status to Expired
		if currentBlockTime.After(expirationDate) {
			cred.Claim.CurrentStatus = "Expired"
			cred.Claim.StatusReason = "Credential Expired"
			cred.Proof.Updated = currentBlockTime.Format(time.RFC3339)

			updatedCredBytes := k.cdc.MustMarshal(&cred)
			store.Set([]byte(cred.Claim.Id), updatedCredBytes)
		}
	}
	return nil
}

Features

Key Management

Key Management is the first that a user need to do, when its about interacting with blockchain. hid-node supports the following ways to store and manage the public keys:

  • test: Public Key information is stored in ${HOME}/.hid-node/keyring-test. This approach is not recommended for production use

  • os: Operating system specific credential sub-system stores the keys encrypted with a passphrase

  • file: It is similar to test backend, with the exception that the keys are encrypted with a passphrase.

Algorithm

secp256k1 algorithm is used in the generation of keys, and it is the only algorithm supported for wallet key generation.

Address Format

The addressing scheme used is bech32 and the address prefix is hid

Commands

  • Run the following to generate a key

hid-noded keys add <name-of-the-key> --keyring-backed <os | test | file>

Save the 24-word mnemonic which is generated in the terminal

If you want to recover a wallet using a Bip39 Mnemonic, append the --recover flag to the above command

  • List all the keys present in a keyring backend

hid-noded keys list --keyring-backed <os | test | file>
  • Delete a key

hid-noded keys delete <name-of-the-key> --keyring-backend <os | test | file>

Token transfer

The x/bank module facilitates the transfer of tokens between accounts. The base denomination used in the chain is uhid, where u represents the SI prefix micro. So, 1 HID represents 1000000uhid. uhid also acts as a governance token. The Coin Type is 118

Run the following to transfer uhid between blockchain accounts

hid-noded tx bank send <source-hid-account> <destination-hid-account> <amount-in-uhid> --chain-id <Chain ID>
  • <source-hid-account> - Source blockchain address

  • <destination-hid-account> - Destination blockchain address

  • <amount-in-uhid> - Amount to be transferred. Example - 1000uhid

Check the balance of an address

hid-noded q bank balances <account-address>

Staking

pageRunning a Testnet Validator Node

Governance

PayTx: Pay fee on someone's behalf

If you want to perform any transaction on chain, you have to pay fees in order to execute it. Adding tokens to your wallet isn't a seamless experience, as you first have to trade your fiat currency with a crypto-currency in a centralised exchange, and then transfer those tokens to your non-custodial wallet, such as Keplr.

The x/authz and x/feegrant modules work together to enable accounts, having no balance, perform blockchain transactions and have the fee be paid by someone who have granted them authorization.

Performing PayTx transactions from CosmJS will be available from the version v0.29.0.

Let's consider a scenario with two actors who will be present on the network:

  • User: They don't possess any uhid tokens

  • Identity Provider (IdP): They possess abundant uhid tokens and will pay the fee for User's MsgCreateDID transaction.

Lets assume the following:

  • <user-addr>: User's blockchain address

  • <idp-addr>: Identity Provider's address

Steps

  • Grant User authorization to perform MsgCreateDID transaction.

hid-noded tx authz grant <user-addr> generic --from <idp-addr> --msg-type "/hypersignprotocol.hidnode.ssi.MsgCreateDID"

List the authorizations

hid-noded q authz grants <idp-addr> <user-addr>
  • Specify the amount of tokens User is allowed to spend in fees. Once this limit exhausts, User cannot perform the authorized transaction

hid-noded tx feegrant grant <idp-addr> <user-addr> --spend-limit 1000uhid

List the fee allowances

hid-noded q feegrant grants <user-addr>
  • Generate a MsgCreateDID transaction and store it in a json file tx.json

hid-noded tx ssi create-did '{
"context": [
"https://www.w3.org/ns/did/v1"
],
"id": "did:hs:zEYJrMxWigf9boyeJMTRN4Ern8DJMoCXaLK77pzQmxVjf",
"controller": ["did:hs:zEYJrMxWigf9boyeJMTRN4Ern8DJMoCXaLK77pzQmxVjf"],
"verificationMethod": [
{
"id": "did:hs:zEYJrMxWigf9boyeJMTRN4Ern8DJMoCXaLK77pzQmxVjf#key-1",
"type": "Ed25519VerificationKey2020",
"controller": "did:hs:zEYJrMxWigf9boyeJMTRN4Ern8DJMoCXaLK77pzQmxVjf",
"publicKeyMultibase": "zEYJrMxWigf9boyeJMTRN4Ern8DJMoCXaLK77pzQmxVjf"
}
],
"authentication": [
"did:hs:zEYJrMxWigf9boyeJMTRN4Ern8DJMoCXaLK77pzQmxVjf#key-1"
]
}' did:hs:zEYJrMxWigf9boyeJMTRN4Ern8DJMoCXaLK77pzQmxVjf#key-1 --ver-key <base64 encoded private key> --from <user-addr> --generate-only > tx.json
  • Execute the MsgCreateDID transaction with fee paying account being Identity Provider

hid-noded tx authz exec tx.json --from <user-addr> --fee-account <idp-addr> --fees 50uhid

The 50uhid is deduced from the Identity Provider account

Cross-chain Token transfer

The cross-chain transfer of tokens between Cosmos-based chains is possible using Inter-Blockchain Communication (IBC) Protocol. Refer the IBC Documentation here.

Run the following to transfer tokens between two accounts located on seperate chains

hid-noded tx ibc-transfer transfer <source-ibc-port> <source-ibc-channel> <destination-chain-wallet-address> <amount> --from <source-chain-wallet-address> 

Architecture

Modes of Operation

The hid-node can be run in three modes:

Full Node

This node stores the complete state of blockchain. However, it does not actively participate in the validation process. Every connected node, by default, is a full node.

Validator Node

It is a full node, which has a significant stake in the network and activately takes part in the validation process.

Seed Node

It's purpose is to introduce newly connected nodes, to other nodes in the network, and disconnects after doing so.

In order to run in seed mode, set pex = true and seed_mode = true in <hid-node config dir>/config/config.toml

Sentry Node Architecture

In order to mitigate DDoS or similar attacks on the validator node, it is recommended to spin a full node, which will be connected to the full node. This architecture is known as Sentry Node Architecture.

The Validator Node is isolated from the outside world, through private subnetting. The Sentry node is connected to the Validator Node. RPC Endpoint and Node ID of Sentry Node are exposed for everyone to connect with. Sentry Node can be thought of as a reverse proxy.

Last updated