👨💻
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.
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
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
.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;
}
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;
}
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;
}
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 blockchainx/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
moduleKeepers 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
}
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
.The following table describes the substores:
Namespace | Key | Value |
---|---|---|
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 |
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
}
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
}
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 useos
: Operating system specific credential sub-system stores the keys encrypted with a passphrasefile
: It is similar totest
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
- 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>
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 accountshid-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>
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.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'sMsgCreateDID
transaction.
Lets assume the following:
<user-addr>
: User's blockchain address<idp-addr>
: Identity Provider's address
Steps
- Grant
User
authorization to performMsgCreateDID
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 filetx.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 accountThe 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>
The
hid-node
can be run in three modes: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.
It is a full node, which has a significant stake in the network and activately takes part in the validation process.
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
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.